Skip to main content

polydat_core/library/
vectors.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Vector dataset access nodes via the `vectordata` crate.
5//!
6//! Each node takes a dataset source string (URL, local path, or
7//! catalog name) as a const parameter and loads the dataset handle at
8//! construction time.
9//!
10//! Source specifier formats:
11//! - `"dataset"` — catalog lookup, uses default profile
12//! - `"dataset:profile"` — catalog lookup with explicit profile
13//! - `"https://..."` — direct URL
14//! - `"/path/to/dir"` — local filesystem path
15//!
16//! ## Prebuffering
17//!
18//! Use `dataset_prebuffer("source")` to eagerly download all facets
19//! for a dataset before workload execution. After prebuffering, data
20//! access uses local mmap readers (zero HTTP overhead).
21//!
22//! ## Cache-aware loading
23//!
24//! For catalog-resolved datasets, the loader checks the local cache
25//! at `~/.cache/vectordata/<dataset>/` before issuing HTTP requests.
26//! Prebuffering populates this cache; subsequent loads are local.
27//!
28//! Feature-gated behind `vectordata`.
29//!
30//! ## Upstream API reference
31//!
32//! See the vectordata consumer API docs for the full dataset access,
33//! catalog, caching, and prebuffer model:
34//! <https://github.com/nosqlbench/vectordata-rs/blob/main/docs/sysref/02-api.md>
35
36use std::sync::{Arc, LazyLock};
37
38use crate::library::support::cache::OnceCache;
39
40use crate::ast::{NodeMeta, PolydatNode, Port, PortType, Slot, Value};
41use vectordata::TestDataGroup;
42use vectordata::TestDataView;
43use vectordata::catalog::resolver::Catalog;
44use vectordata::catalog::sources::CatalogSources;
45use vectordata::io::{VectorReader, VvecReader};
46
47/// Global cache for loaded dataset groups keyed by source string.
48/// Ensures each dataset is loaded exactly once regardless of how many
49/// node functions reference it. The race-free init pattern lives in
50/// [`crate::library::support::cache::OnceCache`].
51static DATASET_CACHE: LazyLock<OnceCache<String, Arc<TestDataGroup>>> =
52    LazyLock::new(OnceCache::new);
53
54/// Type-erased facet cache: (source, profile, facet) → Arc<dyn Any + Send + Sync>.
55/// Ensures each reader (of any element type) is opened exactly once
56/// and shared across all node instances that reference the same data.
57/// The concrete type inside is `Arc<UniformDataset<T>>` or `Arc<Ivvec32Dataset>`
58/// or `Arc<GenericFacetDataset>`. The race-free init pattern lives
59/// in [`OnceCache`].
60type FacetCache = OnceCache<(String, String, String), Arc<dyn std::any::Any + Send + Sync>>;
61static FACET_CACHE: LazyLock<FacetCache> = LazyLock::new(OnceCache::new);
62
63/// Whole-dataset prebuffer cache keyed by source string. Wraps
64/// [`do_dataset_prebuffer_inner`] so N fibers all calling
65/// `dataset_prebuffer("ds:profile")` at init time serialize on
66/// one OnceLock and share the resulting handle. Without this,
67/// each fiber's own kernel-init pass invokes the real prebuffer
68/// body and they all race into the manifest walk + per-facet
69/// download — exactly the thundering herd we hit. The inner
70/// caches (DATASET_CACHE, FACET_CACHE) only protect the
71/// group-resolve and per-facet-reader steps, not the outer
72/// "walk every facet and pull all chunks" work.
73static PREBUFFER_CACHE: LazyLock<OnceCache<String, Arc<DatasetHandle>>> =
74    LazyLock::new(OnceCache::new);
75
76// =================================================================
77// Dataset resolution — catalog-aware, cache-aware
78// =================================================================
79
80/// Parse a source specifier into (dataset_name, profile_name).
81///
82/// Supports `"dataset:profile"` syntax. If no colon is present,
83/// the profile defaults to `"default"`.
84fn parse_source_specifier(source: &str) -> (&str, &str) {
85    // Don't split on colon in URLs
86    if source.starts_with("http://") || source.starts_with("https://") {
87        return (source, "default");
88    }
89    if let Some(pos) = source.find(':') {
90        (&source[..pos], &source[pos + 1..])
91    } else {
92        (source, "default")
93    }
94}
95
96/// Run a synchronous body that may internally drive
97/// `reqwest::blocking` (and thus spin up a private tokio
98/// runtime per HTTP request). If we're sitting on an outer
99/// async runtime — we are, in every per-cycle and per-phase
100/// path — the inner runtime panics on drop with "Cannot drop a
101/// runtime in a context where blocking is not allowed".
102/// `block_in_place` parks the outer multi-thread worker for
103/// the duration of the call so the inner runtime sees a
104/// non-async drop context. Falls back to a direct call when no
105/// runtime is current (e.g. unit tests). Single helper used by
106/// all three vectordata-facing entry points
107/// ([`load_dataset_group`], [`load_uniform_facet`],
108/// [`GenericFacetDataset::load`]).
109fn run_blocking_io<R>(body: impl FnOnce() -> R) -> R {
110    // tokio rides the `vectordata` feature: without the crate there is
111    // no HTTP client to park a worker for.
112    #[cfg(feature = "vectordata")]
113    if tokio::runtime::Handle::try_current().is_ok() {
114        return tokio::task::block_in_place(body);
115    }
116    body()
117}
118
119/// Load a dataset group by name.
120///
121/// Uses the vectordata catalog API: `catalog.open(name)` handles
122/// catalog discovery, cache resolution, and download transparently.
123pub(crate) fn load_dataset_group(source: &str) -> Result<Arc<TestDataGroup>, String> {
124    let (dataset_name, _profile) = parse_source_specifier(source);
125    // Both keys (dataset name and full source spec) point at the
126    // same `Arc<TestDataGroup>`; the `OnceCache` slot for one is
127    // primed by the other on first hit.
128    DATASET_CACHE.get_or_init(dataset_name.to_string(), || {
129        run_blocking_io(|| {
130            let catalog = Catalog::of(&CatalogSources::new().configure_default());
131            catalog
132                .open(dataset_name)
133                .map(Arc::new)
134                .map_err(|e| format!("failed to load dataset '{dataset_name}': {e}"))
135        })
136    })
137}
138
139// =================================================================
140// Dataset handles — loaded once at node construction, shared via Arc
141// =================================================================
142
143/// Generic handle to a loaded uniform vector facet. Thread-safe, random-access.
144/// Supports any element type provided by the vectordata API (f32, f64,
145/// i32, i16, u8, i8, u16, u32, u64, i64, f16).
146pub(crate) struct UniformDataset<T: Send + Sync + 'static> {
147    reader: Arc<dyn VectorReader<T>>,
148    count: usize,
149    dim: usize,
150}
151
152/// Cache-aware loader for uniform vector facets.
153/// Returns a shared `Arc<UniformDataset<T>>`, creating and caching it
154/// on first access. Subsequent loads for the same (source, profile, facet)
155/// return the cached instance.
156fn load_uniform_facet<T: Send + Sync + 'static>(
157    source: &str,
158    profile: &str,
159    facet: &str,
160    open_fn: impl FnOnce(
161        &dyn TestDataView,
162    ) -> std::result::Result<Arc<dyn VectorReader<T>>, vectordata::Error>,
163) -> Result<Arc<UniformDataset<T>>, String> {
164    let key = (source.to_string(), profile.to_string(), facet.to_string());
165    let any = FACET_CACHE.get_or_init(key, || {
166        let group = load_dataset_group(source)?;
167        let view = group
168            .profile(profile)
169            .ok_or_else(|| format!("profile '{profile}' not found in '{source}'"))?;
170        // Audit: log the open *before* `open_fn` runs so the
171        // line appears even if the open errors. Inside
172        // `get_or_init`'s closure, this fires exactly once per
173        // (source, profile, facet) — the prior shape logged
174        // once per concurrent miss (storms of N for N fibers).
175        crate::library::support::audit::record_opened(source, profile, facet, "uniform");
176        let reader = run_blocking_io(|| open_fn(view.as_ref()))
177            .map_err(|e| format!("failed to access {facet} from '{source}': {e}"))?;
178        let count = reader.count();
179        let dim = reader.dim();
180        let arc: Arc<UniformDataset<T>> = Arc::new(UniformDataset { reader, count, dim });
181        Ok(arc as Arc<dyn std::any::Any + Send + Sync>)
182    })?;
183    any.downcast::<UniformDataset<T>>().map_err(|_| {
184        format!(
185            "facet cache type mismatch for '{source}:{profile}/{facet}' — \
186             this should be impossible; please file a bug."
187        )
188    })
189}
190
191// Type aliases for backward compatibility
192type F32Dataset = UniformDataset<f32>;
193type I32Dataset = UniformDataset<i32>;
194
195impl F32Dataset {
196    fn load(source: &str, profile: &str, facet: &str) -> Result<Arc<Self>, String> {
197        let facet_name = facet.to_string();
198        load_uniform_facet(source, profile, facet, move |view| {
199            match facet_name.as_str() {
200                "base" => view.base_vectors(),
201                "query" => view.query_vectors(),
202                "neighbor_distances" => view.neighbor_distances(),
203                "filtered_neighbor_distances" => view.prefiltered_neighbor_distances(),
204                other => Err(vectordata::Error::MissingFacet(format!(
205                    "unknown f32 facet: '{other}'"
206                ))),
207            }
208        })
209    }
210}
211
212impl I32Dataset {
213    fn load(source: &str, profile: &str, facet: &str) -> Result<Arc<Self>, String> {
214        let facet_name = facet.to_string();
215        load_uniform_facet(source, profile, facet, move |view| {
216            match facet_name.as_str() {
217                "neighbor_indices" => view.neighbor_indices(),
218                "filtered_neighbor_indices" => view.prefiltered_neighbor_indices(),
219                other => Err(vectordata::Error::MissingFacet(format!(
220                    "unknown i32 facet: '{other}'"
221                ))),
222            }
223        })
224    }
225}
226
227// =================================================================
228// Dataset handle — typed enum wrapping all per-facet dataset shapes
229// =================================================================
230//
231// Per SRD 53 §"Dataset Handles": `dataset_open(source, facet)` is
232// the resolver node; per-cycle accessors take a handle wire and
233// downcast to the concrete variant they expect. A single Value
234// variant `Value::Handle(Arc<dyn Any>)` carries any of the
235// concrete shapes — the enum below is what's actually inside the
236// Arc, and accessors `match` on the variant.
237//
238// One handle may be opened against multiple facets at the
239// `dataset_open` boundary (the user calls `dataset_open(spec,
240// "base")` vs. `dataset_open(spec, "query")` and gets two
241// distinct handles); the downstream accessor matches on whatever
242// variant came back.
243
244/// Typed wrapper around a resolved dataset facet or group. Held
245/// inside `Value::Handle` as `Arc<DatasetHandle>` and downcast by
246/// accessor nodes via [`Value::as_handle::<DatasetHandle>()`].
247/// Cloning a `Value::Handle` is one `Arc::clone` — one atomic
248/// increment, no allocation — which is the design contract this
249/// enum exists to satisfy.
250#[derive(Clone)]
251pub(crate) enum DatasetHandle {
252    /// Uniform `f32` vector facet (base, query, neighbor_distances,
253    /// filtered_neighbor_distances).
254    F32(Arc<F32Dataset>),
255    /// Uniform `i32` vector facet (neighbor_indices, filtered_neighbor_indices).
256    I32(Arc<I32Dataset>),
257    /// Variable-length `i32` facet (metadata_results).
258    Ivvec32(Arc<Ivvec32Dataset>),
259    /// Type-erased generic-typed scalar facet (metadata_content,
260    /// metadata_predicates, ...).
261    Generic(Arc<GenericFacetDataset>),
262    /// Dataset-group handle — used by group-level metadata
263    /// accessors (`dataset_profile_count`, `dataset_facets`,
264    /// `dataset_distance_function`, ...) that operate on the
265    /// `TestDataGroup` before any profile/facet is chosen.
266    Group(Arc<TestDataGroup>),
267    /// Prebuffered-and-resident dataset, returned by
268    /// `dataset_prebuffer(source)`. Carries both the group AND
269    /// the source spec so per-facet accessors can re-resolve
270    /// (`query_vector_at(prebuffered, q)` → resolves the
271    /// `query` facet from `<source>:<profile>`). Distinct from
272    /// `Group` so existing Group-only consumers stay typed.
273    ///
274    /// The `_group` field keeps the prebuffered `TestDataGroup`
275    /// alive for the duration of the handle — vectordata's
276    /// internal storage cache is keyed off the group instance,
277    /// so dropping the group prematurely would force per-facet
278    /// readers to re-open against transport. The field isn't
279    /// read directly by accessors (they use `source` to re-open
280    /// via `DATASET_CACHE`, which has the same group cached);
281    /// the field's purpose is the lifetime extension.
282    Prebuffered {
283        _group: Arc<TestDataGroup>,
284        source: String,
285    },
286}
287
288impl DatasetHandle {
289    fn open(source: &str, facet: &str) -> Result<Self, String> {
290        let (_, profile) = parse_source_specifier(source);
291        match facet {
292            "base" | "query" | "neighbor_distances" | "filtered_neighbor_distances" => {
293                F32Dataset::load(source, profile, facet).map(DatasetHandle::F32)
294            }
295            "neighbor_indices" | "filtered_neighbor_indices" => {
296                I32Dataset::load(source, profile, facet).map(DatasetHandle::I32)
297            }
298            "metadata_results" => Ivvec32Dataset::load(source, profile).map(DatasetHandle::Ivvec32),
299            // Anything else routes through GenericFacetDataset (typed
300            // scalar reader), which covers metadata_content,
301            // metadata_predicates, and any future scalar facet.
302            _ => GenericFacetDataset::load(source, profile, facet).map(DatasetHandle::Generic),
303        }
304    }
305
306    fn open_group(source: &str) -> Result<Self, String> {
307        load_dataset_group(source).map(DatasetHandle::Group)
308    }
309}
310
311/// Downcast a handle Value to the typed `DatasetHandle` enum.
312///
313/// Surfaces a clear, actionable panic when the upstream slot
314/// holds `Value::None` — that's the canonical signal from a
315/// failed `dataset_open` / `dataset_group_open` (catalog miss,
316/// I/O failure, missing facet on disk, …). Without this
317/// dedicated branch the user would see a generic
318/// `expected Handle, got U64` panic from
319/// [`Value::as_handle`] (None reports U64 as its placeholder
320/// port type), which is six layers removed from the actual
321/// fault. The engine's `enrich_eval_panic` adds node
322/// provenance on top of whichever message we panic with here.
323fn handle_of(v: &Value) -> &DatasetHandle {
324    if matches!(v, Value::None) {
325        panic!(
326            "dataset handle is None — an upstream dataset_open / \
327             dataset_group_open failed to resolve. Check the audit \
328             log for the underlying open error (catalog miss, missing \
329             facet on disk, or transport failure). The dataset's name \
330             is in the audit message; this is the most common fault \
331             when running a workload on a system whose vectordata \
332             catalog isn't configured for the requested dataset."
333        );
334    }
335    v.as_handle::<DatasetHandle>()
336}
337
338/// `dataset_open(source: str, facet: str) -> Handle`
339///
340/// The single resolver node. Provenance follows its two wire
341/// inputs; when both are scope-extern constants (the iter-var
342/// case), this evaluates exactly once at iteration entry and
343/// stays cached for every cycle in that iteration. When source
344/// or facet is cycle-time, it re-evaluates accordingly.
345///
346/// All per-cycle accessors take the resulting handle on a wire
347/// — the catalog/HTTP/mmap path is never on the cycle hot path.
348///
349/// Resolve failures used to fall through silently as `Value::None`
350/// so a downstream op wrapper could lift them. That pattern also
351/// let comprehension clause evaluation degrade silently (catalog
352/// miss → None → downstream `handle_of` panic → caught + swallowed
353/// → literal-list fallback splits the spec on a comma → garbage
354/// iter-var). We now panic with the underlying error: the engine's
355/// `enrich_eval_panic` adds node provenance, `eval_const_expr`
356/// traps the panic into `Result::Err`, and `evaluate_spec`
357/// propagates it as a clean clause-level diagnostic.
358#[crate::polydat_node(category = RealData)]
359fn dataset_open(source: &str, facet: &str) -> Arc<DatasetHandle> {
360    match DatasetHandle::open(source, facet) {
361        Ok(h) => Arc::new(h),
362        Err(e) => {
363            let msg = format!("dataset_open: failed to resolve '{source}' facet='{facet}': {e}");
364            crate::library::support::audit::error(&msg);
365            panic!("{msg}");
366        }
367    }
368}
369
370/// `dataset_group_open(source: str) -> Handle`
371///
372/// Group-level resolver. Returns a handle wrapping `Arc<TestDataGroup>`
373/// — used by group-level metadata accessors (`dataset_profile_count`,
374/// `dataset_facets`, ...) that operate on the dataset as a whole
375/// before any profile/facet is selected.
376///
377/// Hard-fail on resolve failure — see `dataset_open` for the
378/// rationale. The Value::None pattern was a silent-degradation
379/// source for comprehension clause evaluation.
380#[crate::polydat_node(category = RealData)]
381fn dataset_group_open(source: &str) -> Arc<DatasetHandle> {
382    match DatasetHandle::open_group(source) {
383        Ok(h) => Arc::new(h),
384        Err(e) => {
385            let msg = format!("dataset_group_open: failed to resolve '{source}': {e}");
386            crate::library::support::audit::error(&msg);
387            panic!("{msg}");
388        }
389    }
390}
391
392// =================================================================
393// Base vector nodes
394// =================================================================
395
396// All indexed-accessor nodes share the same shape: an `index`
397// wire (u64) and a `source` wire (Str), with the dataset
398// resolved lazily on first eval per spec via `DATASET_CACHE`
399// (inside `F32Dataset::load` / `I32Dataset::load`). The
400// per-cycle hot path is one HashMap lookup on the cached spec
401// plus the existing facet read; the spec doesn't change within
402// an iteration scope so subsequent cycles hit the cache.
403
404// =================================================================
405// Per-cycle indexed accessors
406// =================================================================
407//
408// All take `(handle: Handle, index: u64)`. The handle is resolved
409// once per iteration by `dataset_open` (or by cursor sugar that
410// emits an implicit one); per-cycle eval downcasts the handle's
411// `Arc<DatasetHandle>` and reads at `index`. No HashMap, no Mutex,
412// no String allocation on the source side. The `Bytes` outputs
413// allocate a fresh per-cycle buffer — that's the remaining
414// per-cycle alloc until SRD 46 (native vector binding).
415//
416// `expected_variant` documents which `DatasetHandle` variant is
417// expected; a panic on wrong variant indicates the workload
418// opened a handle for a different facet than the accessor expects.
419
420macro_rules! handle_indexed_node {
421    (
422        $(#[$meta:meta])*
423        $name:ident, $func_name:literal, $out_port:ident,
424        facet = $facet:literal,
425        eval = $eval_fn:expr
426    ) => {
427        $(#[$meta])*
428        pub struct $name {
429            meta: NodeMeta,
430        }
431
432        impl $name {
433            /// A node of this kind.
434            pub fn new() -> Self {
435                Self {
436                    meta: NodeMeta {
437                        name: $func_name.into(),
438                        outs: vec![Port::new("output", PortType::$out_port)],
439                        ins: vec![
440                            Slot::Wire(Port::handle("handle")),
441                            Slot::Wire(Port::u64("index")),
442                        ],
443                    },
444                }
445            }
446        }
447
448        impl Default for $name {
449            fn default() -> Self { Self::new() }
450        }
451
452        impl PolydatNode for $name {
453            fn meta(&self) -> &NodeMeta { &self.meta }
454            fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
455                let handle = handle_of(&inputs[0]);
456                // If the handle is `Prebuffered` (returned by
457                // `dataset_prebuffer`), resolve to the specific
458                // facet variant the accessor expects. For all
459                // other handles this is a no-op borrow.
460                let resolved = handle.resolve_facet($facet);
461                let index = inputs[1].as_u64() as usize;
462                outputs[0] = ($eval_fn)(resolved.as_ref(), index);
463            }
464        }
465    };
466}
467
468/// Resolve a Prebuffered handle to a specific-facet handle by
469/// opening the named facet via the existing FACET_CACHE path
470/// (which is `OnceCache`-backed — concurrent callers serialize
471/// per (source, profile, facet)). For non-Prebuffered handles
472/// this is a borrow-through; the macro callers use the result as
473/// `&DatasetHandle` regardless of which arm fires.
474///
475/// Lives on `DatasetHandle` so the resolution logic — and the
476/// "what does Prebuffered mean to a per-facet accessor" question
477/// — sits next to the variant declaration.
478impl DatasetHandle {
479    fn resolve_facet<'a>(&'a self, facet: &str) -> std::borrow::Cow<'a, DatasetHandle> {
480        match self {
481            DatasetHandle::Prebuffered { source, .. } => match DatasetHandle::open(source, facet) {
482                Ok(opened) => std::borrow::Cow::Owned(opened),
483                Err(e) => panic!(
484                    "DatasetHandle::resolve_facet: failed to open \
485                         '{facet}' from prebuffered '{source}': {e}"
486                ),
487            },
488            _ => std::borrow::Cow::Borrowed(self),
489        }
490    }
491}
492
493fn f32_vec_at(h: &DatasetHandle, index: usize) -> Value {
494    match h {
495        DatasetHandle::F32(d) => Value::VecF32(slice_arc_from_uniform(d, index)),
496        other => panic!(
497            "expected F32 dataset handle, got {}",
498            dataset_handle_kind(other)
499        ),
500    }
501}
502
503fn i32_vec_at(h: &DatasetHandle, index: usize) -> Value {
504    match h {
505        DatasetHandle::I32(d) => Value::VecI32(slice_arc_from_uniform(d, index)),
506        other => panic!(
507            "expected I32 dataset handle, got {}",
508            dataset_handle_kind(other)
509        ),
510    }
511}
512
513fn ivvec32_vec_at(h: &DatasetHandle, index: usize) -> Value {
514    match h {
515        // Variable-length records — vectordata's trait doesn't expose
516        // a zero-copy slice path for these (per-record dim is read
517        // from the file), so we always allocate. One Vec<i32> per
518        // cycle; same bound as the upstream trait API.
519        DatasetHandle::Ivvec32(d) => {
520            if d.count == 0 {
521                return Value::VecI32(crate::ast::SliceArc::from_vec(Vec::<i32>::new()));
522            }
523            let v = d.reader.get(index % d.count).unwrap_or_default();
524            Value::VecI32(crate::ast::SliceArc::from_vec(v))
525        }
526        other => panic!(
527            "expected Ivvec32 dataset handle, got {}",
528            dataset_handle_kind(other)
529        ),
530    }
531}
532
533/// Build a [`SliceArc<T>`] from any uniform-stride dataset at
534/// `index`. Single generic helper consolidating what used to be
535/// per-element-type slice-arc constructors — element type is
536/// erased by the upstream `VectorReader<T>` trait, and `SliceArc`
537/// is element-type-generic, so one body suffices for `f32`,
538/// `i32`, and any other element type the trait supports.
539///
540/// Tries the zero-copy `VectorReader::get_slice` path first —
541/// that returns a borrow into the mmap'd file pages, kept alive
542/// by the `Arc<UniformDataset<T>>` that we move into the
543/// `SliceArc` as owner. No allocation, no element decoding, no
544/// copy.
545///
546/// Falls back to `VectorReader::get(idx) -> Vec<T>` for readers
547/// that don't support zero-copy (HTTP-backed, or merkle-cached
548/// storage that hasn't been promoted to mmap yet); that path
549/// allocates one `Vec<T>` per cycle.
550fn slice_arc_from_uniform<T>(d: &Arc<UniformDataset<T>>, index: usize) -> crate::ast::SliceArc<T>
551where
552    T: Send + Sync + Copy + 'static,
553{
554    if d.count == 0 {
555        return crate::ast::SliceArc::from_vec(Vec::<T>::new());
556    }
557    let idx = index % d.count;
558    if let Some(slice) = d.reader.get_slice(idx) {
559        // SAFETY: the slice points into the mmap pages owned by
560        // `d`'s reader; cloning `d` into the `SliceArc`'s owner
561        // keeps the mmap alive for as long as the slice is held.
562        let ptr_len = (slice.as_ptr(), slice.len());
563        let owner = d.clone();
564        let owner_dyn: Arc<dyn std::any::Any + Send + Sync> = owner;
565        return unsafe {
566            crate::ast::SliceArc::from_borrowed(
567                owner_dyn,
568                std::slice::from_raw_parts(ptr_len.0, ptr_len.1),
569            )
570        };
571    }
572    crate::ast::SliceArc::from_vec(d.reader.get(idx).unwrap_or_default())
573}
574
575fn dataset_handle_kind(h: &DatasetHandle) -> &'static str {
576    match h {
577        DatasetHandle::F32(_) => "F32",
578        DatasetHandle::I32(_) => "I32",
579        DatasetHandle::Ivvec32(_) => "Ivvec32",
580        DatasetHandle::Generic(_) => "Generic",
581        DatasetHandle::Group(_) => "Group",
582        DatasetHandle::Prebuffered { .. } => "Prebuffered",
583    }
584}
585
586/// Helper for group-level accessors: extract the `TestDataGroup`
587/// from a `DatasetHandle::Group` variant. Panics on mismatch.
588fn group_of(handle: &DatasetHandle) -> &TestDataGroup {
589    match handle {
590        DatasetHandle::Group(g) => g.as_ref(),
591        other => panic!("expected Group handle, got {}", dataset_handle_kind(other)),
592    }
593}
594
595handle_indexed_node!(
596    /// Access an `f32` vector by index, returning a typed `VecF32`.
597    /// Works on any F32 handle (base or query facet).
598    ///
599    /// Signature: `vector_at(handle, index: u64) -> VecF32`
600    VectorAt, "vector_at", VecF32, facet = "base", eval = f32_vec_at
601);
602
603handle_indexed_node!(
604    /// Access a query vector by index. Alias for [`VectorAt`] kept
605    /// for clarity in workloads that distinguish base and query
606    /// handles by name.
607    ///
608    /// Signature: `query_vector_at(handle, index: u64) -> VecF32`
609    QueryVectorAt, "query_vector_at", VecF32, facet = "query", eval = f32_vec_at
610);
611
612handle_indexed_node!(
613    /// Access ground-truth neighbor indices for a query. Expects an
614    /// I32 handle.
615    ///
616    /// Signature: `neighbor_indices_at(handle, index: u64) -> VecI32`
617    NeighborIndicesAt, "neighbor_indices_at", VecI32, facet = "neighbor_indices", eval = i32_vec_at
618);
619
620handle_indexed_node!(
621    /// Access ground-truth neighbor distances for a query. Expects an
622    /// F32 handle.
623    ///
624    /// Signature: `neighbor_distances_at(handle, index: u64) -> VecF32`
625    NeighborDistancesAt, "neighbor_distances_at", VecF32, facet = "neighbor_distances", eval = f32_vec_at
626);
627
628handle_indexed_node!(
629    /// Access filtered ground-truth neighbor indices. Expects an I32 handle.
630    FilteredNeighborIndicesAt, "filtered_neighbor_indices_at", VecI32, facet = "filtered_neighbor_indices", eval = i32_vec_at
631);
632
633handle_indexed_node!(
634    /// Access filtered ground-truth neighbor distances. Expects an F32 handle.
635    FilteredNeighborDistancesAt, "filtered_neighbor_distances_at", VecF32, facet = "filtered_neighbor_distances", eval = f32_vec_at
636);
637
638// =================================================================
639// Metadata nodes (constant per dataset)
640// =================================================================
641
642// =================================================================
643// Per-handle metadata nodes
644// =================================================================
645//
646// Take `(handle: Handle)` and read shape/metadata directly from the
647// resolved dataset. Provenance bounded by the handle, which itself
648// is bounded by the resolver's externs — so these collapse to a
649// single per-iteration eval chain via the standard provenance
650// caching. No per-cycle work.
651
652macro_rules! handle_metadata_node {
653    (
654        $(#[$meta:meta])*
655        $name:ident, $func_name:literal, $out_port:ident,
656        eval = $eval_fn:expr
657    ) => {
658        $(#[$meta])*
659        pub struct $name {
660            meta: NodeMeta,
661        }
662
663        impl $name {
664            /// A node of this kind.
665            pub fn new() -> Self {
666                Self {
667                    meta: NodeMeta {
668                        name: $func_name.into(),
669                        outs: vec![Port::new("output", PortType::$out_port)],
670                        ins: vec![Slot::Wire(Port::handle("handle"))],
671                    },
672                }
673            }
674        }
675
676        impl Default for $name {
677            fn default() -> Self { Self::new() }
678        }
679
680        impl PolydatNode for $name {
681            fn meta(&self) -> &NodeMeta { &self.meta }
682            fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
683                let handle = handle_of(&inputs[0]);
684                outputs[0] = ($eval_fn)(handle);
685            }
686        }
687    };
688}
689
690handle_metadata_node!(
691    /// Return the dimensionality (`f32` count per record) of a
692    /// vector facet handle.
693    ///
694    /// Signature: `vector_dim(handle) -> (u64)`
695    VectorDim, "vector_dim", U64,
696    eval = |h: &DatasetHandle| match h {
697        DatasetHandle::F32(d) => Value::U64(d.dim as u64),
698        DatasetHandle::I32(d) => Value::U64(d.dim as u64),
699        _ => Value::U64(0),
700    }
701);
702
703/// Return the dataset's distance function (e.g., "cosine", "euclidean").
704///
705/// Signature: `dataset_distance_function(source) -> (String)`
706// Source-only nodes (just take a `source` String wire) all
707// share the new shape: declare one Wire slot, read inputs[0]
708// at eval, look up via the global cache, return the property.
709macro_rules! source_only_node {
710    (
711        $(#[$meta:meta])*
712        $name:ident, $func_name:literal,
713        out_port = $out:ident,
714        eval = |$src:ident| $body:expr
715    ) => {
716        $(#[$meta])*
717        pub struct $name {
718            meta: NodeMeta,
719        }
720
721        impl $name {
722            /// A node of this kind.
723            pub fn new() -> Self {
724                Self {
725                    meta: NodeMeta {
726                        name: $func_name.into(),
727                        outs: vec![Port::new("output", PortType::$out)],
728                        ins: vec![Slot::Wire(Port::str("source"))],
729                    },
730                }
731            }
732        }
733
734        impl Default for $name {
735            fn default() -> Self { Self::new() }
736        }
737
738        impl PolydatNode for $name {
739            fn meta(&self) -> &NodeMeta { &self.meta }
740            fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
741                let $src = inputs[0].as_str();
742                outputs[0] = $body;
743            }
744        }
745    };
746}
747
748// Helper: read a dataset-group attribute via a handle's underlying
749// `TestDataGroup`. Used by `dataset_distance_function`. We cache
750// the source string on the dataset handle itself to keep
751// dataset-group attributes accessible without an extra lookup —
752// but for now the simpler path is to require a separate
753// dataset-group-level node that takes `source: str` (kept below).
754
755handle_metadata_node!(
756    /// Return the count of records in the facet a handle was opened
757    /// against. This is the canonical "how many vectors / queries /
758    /// neighbor-rows" accessor — `vector_count(base_handle)` for
759    /// base vectors, `vector_count(query_handle)` for query
760    /// vectors, etc.
761    ///
762    /// Signature: `vector_count(handle) -> (u64)`
763    VectorCount, "vector_count", U64,
764    eval = |h: &DatasetHandle| match h {
765        DatasetHandle::F32(d) => Value::U64(d.count as u64),
766        DatasetHandle::I32(d) => Value::U64(d.count as u64),
767        DatasetHandle::Ivvec32(d) => Value::U64(d.count as u64),
768        DatasetHandle::Generic(d) => Value::U64(d.count as u64),
769        DatasetHandle::Group(_) => panic!("vector_count: expected facet handle, got Group"),
770        DatasetHandle::Prebuffered { source, .. } => {
771            // Resolve the `base` facet (canonical interpretation
772            // of `vector_count(prebuffered)` — base vectors).
773            match DatasetHandle::open(source, "base") {
774                Ok(DatasetHandle::F32(d)) => Value::U64(d.count as u64),
775                Ok(other) => panic!(
776                    "vector_count: expected F32 base facet, got {}",
777                    dataset_handle_kind(&other)),
778                Err(e) => panic!(
779                    "vector_count: failed to open 'base' from prebuffered '{source}': {e}"),
780            }
781        }
782    }
783);
784
785handle_metadata_node!(
786    /// Alias for [`VectorCount`] kept for clarity in workloads that
787    /// distinguish base/query handles by name.
788    ///
789    /// Signature: `query_count(handle) -> (u64)`
790    QueryCount, "query_count", U64,
791    eval = |h: &DatasetHandle| match h {
792        DatasetHandle::F32(d) => Value::U64(d.count as u64),
793        DatasetHandle::I32(d) => Value::U64(d.count as u64),
794        DatasetHandle::Ivvec32(d) => Value::U64(d.count as u64),
795        DatasetHandle::Generic(d) => Value::U64(d.count as u64),
796        DatasetHandle::Group(_) => panic!("query_count: expected facet handle, got Group"),
797        DatasetHandle::Prebuffered { source, .. } => {
798            // `query_count(prebuffered)` is a legitimate idiom in the
799            // pvs_query workload (`cursor q = range(0, query_count(...))`),
800            // so resolve to the query-facet handle and read its count.
801            // Same OnceCache-backed open as the per-cycle accessors.
802            match DatasetHandle::open(source, "query") {
803                Ok(DatasetHandle::F32(d)) => Value::U64(d.count as u64),
804                Ok(other) => panic!(
805                    "query_count: expected F32 query facet, got {}",
806                    dataset_handle_kind(&other)),
807                Err(e) => panic!(
808                    "query_count: failed to open 'query' from prebuffered '{source}': {e}"),
809            }
810        }
811    }
812);
813
814handle_metadata_node!(
815    /// Return the per-record neighbor count (k) for an I32
816    /// neighbor-indices handle.
817    ///
818    /// Signature: `neighbor_count(handle) -> (u64)`
819    NeighborCount, "neighbor_count", U64,
820    eval = |h: &DatasetHandle| match h {
821        DatasetHandle::I32(d) => Value::U64(d.dim as u64),
822        _ => Value::U64(0),
823    }
824);
825
826handle_metadata_node!(
827    /// Return the dataset's distance function (e.g., "COSINE",
828    /// "EUCLIDEAN"). Operates on the dataset group; takes a Group
829    /// handle.
830    ///
831    /// Signature: `dataset_distance_function(group) -> (String)`
832    DatasetDistanceFunction, "dataset_distance_function", Str,
833    eval = |h: &DatasetHandle| {
834        let group = group_of(h);
835        let raw = group
836            .attribute("distance_function")
837            .and_then(|v| v.as_str())
838            .unwrap_or("unknown");
839        let df = match raw.to_uppercase().as_str() {
840            "L2" | "EUCLIDEAN" => "EUCLIDEAN",
841            "L1" | "MANHATTAN" => "MANHATTAN",
842            "COSINE" => "COSINE",
843            "DOT_PRODUCT" | "DOTPRODUCT" | "DOT" | "INNER_PRODUCT" | "IP" => "DOT_PRODUCT",
844            _ => raw,
845        };
846        Value::Str(df.to_string().into())
847    }
848);
849
850// =================================================================
851// Metadata facet nodes
852// =================================================================
853
854/// Handle to a loaded variable-length i32 facet (metadata_results).
855pub(crate) struct Ivvec32Dataset {
856    reader: Arc<dyn VvecReader<i32>>,
857    count: usize,
858}
859
860impl Ivvec32Dataset {
861    fn load(source: &str, profile: &str) -> Result<Arc<Self>, String> {
862        let key = (
863            source.to_string(),
864            profile.to_string(),
865            "metadata_results".to_string(),
866        );
867        let any = FACET_CACHE.get_or_init(key, || {
868            let group = load_dataset_group(source)?;
869            let view = group
870                .profile(profile)
871                .ok_or_else(|| format!("profile '{profile}' not found in '{source}'"))?;
872            crate::library::support::audit::record_opened(
873                source,
874                profile,
875                "metadata_results",
876                "ivvec32",
877            );
878            let reader = run_blocking_io(|| view.metadata_results())
879                .map_err(|e| format!("failed to access metadata_results from '{source}': {e}"))?;
880            let count = reader.count();
881            let arc: Arc<Self> = Arc::new(Self { reader, count });
882            Ok(arc as Arc<dyn std::any::Any + Send + Sync>)
883        })?;
884        any.downcast::<Self>().map_err(|_| {
885            format!("facet cache type mismatch for '{source}:{profile}/metadata_results'")
886        })
887    }
888}
889
890handle_indexed_node!(
891    /// Access metadata indices (variable-length matching base ordinals
892    /// per query) at an index. Expects an Ivvec32 handle.
893    ///
894    /// Signature: `metadata_results_at(handle, index: u64) -> VecI32`
895    MetadataResultsAt, "metadata_results_at", VecI32, facet = "metadata_results", eval = ivvec32_vec_at
896);
897
898handle_indexed_node!(
899    /// Return the length of one metadata_results record without
900    /// loading the data (reads only the 4-byte header). Expects an
901    /// Ivvec32 handle.
902    ///
903    /// Signature: `metadata_results_len_at(handle, index: u64) -> (u64)`
904    MetadataResultsLenAt, "metadata_results_len_at", U64, facet = "metadata_results",
905    eval = |h: &DatasetHandle, idx: usize| match h {
906        DatasetHandle::Ivvec32(d) if d.count > 0 => {
907            let len = d.reader.dim_at(idx % d.count).unwrap_or(0);
908            Value::U64(len as u64)
909        }
910        _ => Value::U64(0),
911    }
912);
913
914handle_metadata_node!(
915    /// Return the metadata indices count (number of predicate result
916    /// sets). Expects an Ivvec32 handle.
917    MetadataResultsCount, "metadata_results_count", U64,
918    eval = |h: &DatasetHandle| match h {
919        DatasetHandle::Ivvec32(d) => Value::U64(d.count as u64),
920        _ => Value::U64(0),
921    }
922);
923
924handle_metadata_node!(
925    /// Report which facets are available for the default profile of
926    /// a dataset group as a comma-separated list. Expects a Group
927    /// handle.
928    ///
929    /// Signature: `dataset_facets(group) -> (String)`
930    DatasetFacets, "dataset_facets", Str,
931    eval = |h: &DatasetHandle| {
932        let group = group_of(h);
933        // Use the default profile — group-level callers want a
934        // dataset-wide manifest; specific profile facets come via
935        // `profile_facets(group, idx)`.
936        let names = group.profile_names();
937        if let Some(first) = names.first()
938            && let Some(view) = group.profile(first) {
939                let manifest = view.facet_manifest();
940                let mut names: Vec<&String> = manifest.keys().collect();
941                names.sort();
942                return Value::Str(
943                    names.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ").into()
944                );
945            }
946        Value::Str(String::new().into())
947    }
948);
949
950// =================================================================
951// Profile enumeration — discover and iterate over dataset profiles
952// =================================================================
953//
954// Group-level: take a Group handle, read the dataset's sorted profile
955// list. Sort order is canonical (by base_count via `profile_sort_by_size`)
956// and computed once per group via the underlying TestDataGroup.
957
958handle_metadata_node!(
959    /// Total number of profiles in a dataset group.
960    ///
961    /// Signature: `dataset_profile_count(group) -> (u64)`
962    DatasetProfileCount, "dataset_profile_count", U64,
963    eval = |h: &DatasetHandle| Value::U64(group_of(h).profile_names().len() as u64)
964);
965
966handle_metadata_node!(
967    /// Comma-separated list of all profile names in canonical sort
968    /// order (by base_count).
969    ///
970    /// Signature: `dataset_profile_names(group) -> (String)`
971    DatasetProfileNames, "dataset_profile_names", Str,
972    eval = |h: &DatasetHandle| Value::Str(group_of(h).profile_names().join(", ").into())
973);
974
975/// Return profile names matching a prefix, comma-separated.
976///
977/// Signature: `matching_profiles(group, prefix: str) -> (String)`
978///
979/// If prefix is empty, returns all profiles. Used by `for_each:`
980/// phase templates to discover profiles dynamically.
981///
982/// `group` declares its source-string auto-resolver via the
983/// `Resolved<GroupResolver, _>` marker wrapper — the macro reads
984/// `<Resolved<GroupResolver, DatasetHandle> as Wire>::RESOLVER` at
985/// codegen and emits the matching `FuncSig.default_resolver`
986/// (`DefaultResolver::Group`). The spliced `dataset_group_open` yields
987/// the canonical `Value::Handle(Arc<DatasetHandle>)`, so the wire is
988/// resolved as `DatasetHandle` and the group is taken via `group_of`
989/// (downcasting straight to `TestDataGroup` would fail — the handle is
990/// always the unified `DatasetHandle` enum).
991#[crate::polydat_node(category = RealData)]
992fn matching_profiles(
993    group: crate::derive_support::Resolved<crate::derive_support::GroupResolver, DatasetHandle>,
994    prefix: &str,
995) -> String {
996    let group: &TestDataGroup = group_of(&group);
997    let all = group.profile_names();
998    let mut matched: Vec<&str> = if prefix.is_empty() {
999        all.iter().map(|s| s.as_str()).collect()
1000    } else {
1001        all.iter()
1002            .filter(|s| s.starts_with(prefix))
1003            .map(|s| s.as_str())
1004            .collect()
1005    };
1006    // Natural-order sort: alphabetic with numeric runs
1007    // compared as numbers so `label_03` sorts before
1008    // `label_10`. The upstream `group.profile_names()`
1009    // orders by `base_count` (vectordata's choice — useful
1010    // for index-based lookups), but `for_each` iteration
1011    // wants stable, human-natural order so users see
1012    // label_01, label_02, label_03 instead of whatever the
1013    // size-sort happens to produce.
1014    matched.sort_by(|a, b| natural_cmp(a, b));
1015    matched.join(",")
1016}
1017
1018/// Natural ordering: split each string into alternating text
1019/// and numeric runs and compare run-by-run, comparing numeric
1020/// runs as integers. Beats lexicographic on `label_03` vs
1021/// `label_10` (lex: "10" < "3"; natural: 3 < 10).
1022fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering {
1023    let mut ai = a.chars().peekable();
1024    let mut bi = b.chars().peekable();
1025    loop {
1026        match (ai.peek().copied(), bi.peek().copied()) {
1027            (None, None) => return std::cmp::Ordering::Equal,
1028            (None, _) => return std::cmp::Ordering::Less,
1029            (_, None) => return std::cmp::Ordering::Greater,
1030            (Some(ac), Some(bc)) => {
1031                if ac.is_ascii_digit() && bc.is_ascii_digit() {
1032                    let mut na: u64 = 0;
1033                    while let Some(c) = ai.peek().copied()
1034                        && c.is_ascii_digit()
1035                    {
1036                        na = na
1037                            .saturating_mul(10)
1038                            .saturating_add((c as u8 - b'0') as u64);
1039                        ai.next();
1040                    }
1041                    let mut nb: u64 = 0;
1042                    while let Some(c) = bi.peek().copied()
1043                        && c.is_ascii_digit()
1044                    {
1045                        nb = nb
1046                            .saturating_mul(10)
1047                            .saturating_add((c as u8 - b'0') as u64);
1048                        bi.next();
1049                    }
1050                    match na.cmp(&nb) {
1051                        std::cmp::Ordering::Equal => continue,
1052                        non_eq => return non_eq,
1053                    }
1054                } else {
1055                    match ac.cmp(&bc) {
1056                        std::cmp::Ordering::Equal => {
1057                            ai.next();
1058                            bi.next();
1059                        }
1060                        non_eq => return non_eq,
1061                    }
1062                }
1063            }
1064        }
1065    }
1066}
1067
1068/// Look up a profile name by index from the canonical sorted list.
1069///
1070/// Signature: `dataset_profile_name_at(group, index: u64) -> (String)`
1071///
1072/// Index wraps modulo the number of profiles.
1073#[crate::polydat_node(category = RealData)]
1074fn dataset_profile_name_at(
1075    group: crate::derive_support::Resolved<crate::derive_support::GroupResolver, DatasetHandle>,
1076    index: u64,
1077) -> String {
1078    let group: &TestDataGroup = group_of(&group);
1079    let names = group.profile_names();
1080    if names.is_empty() {
1081        String::new()
1082    } else {
1083        names[(index as usize) % names.len()].clone()
1084    }
1085}
1086
1087/// Return the base vector count for the profile at a given index.
1088///
1089/// Signature: `profile_base_count(group, index: u64) -> (u64)`
1090#[crate::polydat_node(category = RealData)]
1091fn profile_base_count(
1092    group: crate::derive_support::Resolved<crate::derive_support::GroupResolver, DatasetHandle>,
1093    index: u64,
1094) -> u64 {
1095    let group: &TestDataGroup = group_of(&group);
1096    let names = group.profile_names();
1097    if names.is_empty() {
1098        0
1099    } else {
1100        let name = &names[(index as usize) % names.len()];
1101        group
1102            .profile(name)
1103            .and_then(|view| view.base_count())
1104            .unwrap_or(0)
1105    }
1106}
1107
1108/// Return the comma-separated facet list for the profile at a given index.
1109///
1110/// Signature: `profile_facets(group, index: u64) -> (String)`
1111#[crate::polydat_node(category = RealData)]
1112fn profile_facets(
1113    group: crate::derive_support::Resolved<crate::derive_support::GroupResolver, DatasetHandle>,
1114    index: u64,
1115) -> String {
1116    let group: &TestDataGroup = group_of(&group);
1117    let names = group.profile_names();
1118    if names.is_empty() {
1119        String::new()
1120    } else {
1121        let name = &names[(index as usize) % names.len()];
1122        match group.profile(name) {
1123            Some(view) => {
1124                let manifest = view.facet_manifest();
1125                let mut fnames: Vec<&String> = manifest.keys().collect();
1126                fnames.sort();
1127                fnames
1128                    .iter()
1129                    .map(|s| s.as_str())
1130                    .collect::<Vec<_>>()
1131                    .join(", ")
1132            }
1133            None => String::new(),
1134        }
1135    }
1136}
1137
1138/// Partition a dataset's vector space by its **profiles matching a
1139/// pattern**, treated as cumulative size tiers (an SRD-71 partition
1140/// source). One partition per masked profile, in canonical
1141/// (base-count-ascending) order: partition `k` spans
1142/// `[prev_masked_base_count, this_masked_base_count)` — exactly the
1143/// vectors added at that tier — so a sweep's "load only the increment
1144/// since the previously loaded set" is just the partition's
1145/// `[start_of(p), end_of(p))`, and a partition inherently knows its
1146/// start (no cross-iteration carry needed). `idx_of(p)` is the 0-based
1147/// masked position (pairs with `matching_profile_name_at` to address
1148/// the tier's own ground-truth facets); `count_of(p)` is the number of
1149/// masked tiers; `base_extent` is the largest masked tier's size.
1150///
1151/// `pattern` follows the literal / glob / regex promotion of
1152/// [`crate::library::support::pattern::compile_pattern`]; `*` (or any pattern that
1153/// matches all names) selects every profile.
1154///
1155/// Signature: `profile_partitions(group, pattern: str) -> (PartitionList)`
1156#[crate::polydat_node(category = RealData)]
1157fn profile_partitions(
1158    group: crate::derive_support::Resolved<crate::derive_support::GroupResolver, DatasetHandle>,
1159    pattern: &str,
1160) -> crate::derive_support::Ext<crate::iteration::cursor_partition::PartitionList> {
1161    let group: &TestDataGroup = group_of(&group);
1162    let parts = build_profile_partitions(group, pattern);
1163    crate::derive_support::Ext(crate::iteration::cursor_partition::PartitionList::new(
1164        parts,
1165    ))
1166}
1167
1168/// Build the cumulative size-tier partitions for the profiles of `group`
1169/// matching `pattern` (literal/glob/regex promotion). Shared by the
1170/// [`profile_partitions`] node and the comprehension-source desugaring
1171/// in `iteration::comprehension::eval` so a `for: "p in
1172/// profile_partitions(...)"` sweep produces identical partitions either
1173/// way. Partition `k` spans `[prev masked base_count, this base_count)`.
1174/// The masked profile size-tiers for `group` matching `pattern`, in
1175/// canonical (base-count-ascending) order: `(name, cumulative_base_count)`
1176/// for each matching profile WITH a non-zero base count. Profiles with no
1177/// base facet, or a zero count, are skipped — they would otherwise emit a
1178/// degenerate empty `[prev, prev)` partition (and a query against an empty
1179/// tier). Shared by [`build_profile_partitions`] and
1180/// [`matching_profile_name_at`] so a partition's `idx_of(p)` and the tier
1181/// name resolve against the SAME masked sequence.
1182fn masked_profile_tiers(group: &TestDataGroup, pattern: &str) -> Vec<(String, u64)> {
1183    let (re, _) = crate::library::support::pattern::compile_pattern(pattern)
1184        .unwrap_or_else(|e| panic!("profile pattern: {e}"));
1185    group
1186        .profile_names()
1187        .iter()
1188        .filter(|n| re.is_match(n))
1189        .filter_map(|n| {
1190            group
1191                .profile(n)
1192                .and_then(|v| v.base_count())
1193                .filter(|&c| c > 0)
1194                .map(|c| (n.clone(), c))
1195        })
1196        .collect()
1197}
1198
1199pub(crate) fn build_profile_partitions(
1200    group: &TestDataGroup,
1201    pattern: &str,
1202) -> Vec<crate::iteration::cursor_partition::Partition> {
1203    // Masked size-tiers (matching profiles with a non-zero base count),
1204    // canonical (ascending) order — skipping zero-count profiles avoids a
1205    // degenerate empty first partition.
1206    let masked: Vec<u64> = masked_profile_tiers(group, pattern)
1207        .into_iter()
1208        .map(|(_, c)| c)
1209        .collect();
1210    let base_extent = masked.last().copied().unwrap_or(0);
1211    let count = masked.len() as u64;
1212    let pct = |o: u64| {
1213        if base_extent == 0 {
1214            0.0
1215        } else {
1216            (o as f64 / base_extent as f64) * 100.0
1217        }
1218    };
1219    let mut parts: Vec<crate::iteration::cursor_partition::Partition> =
1220        Vec::with_capacity(masked.len());
1221    let mut prev: u64 = 0;
1222    for (k, &this) in masked.iter().enumerate() {
1223        parts.push(crate::iteration::cursor_partition::Partition {
1224            idx: k as u64,
1225            count,
1226            start_ord: prev,
1227            end_ord: this,
1228            start_pct: pct(prev),
1229            end_pct: pct(this),
1230            base_extent,
1231        });
1232        prev = this;
1233    }
1234    parts
1235}
1236
1237/// Name of the `index`-th profile **matching `pattern`**, in canonical
1238/// (base-count-ascending) order. Pairs with `profile_partitions`'s
1239/// masked-position `idx_of` so a sweep can prebuffer the active tier's
1240/// own ground-truth facets (`dataset_prebuffer(str_concat("ds:", name))`).
1241/// `pattern` follows the literal / glob / regex promotion; the index
1242/// wraps modulo the number of matching profiles; empty string if none
1243/// match.
1244///
1245/// Signature: `matching_profile_name_at(group, pattern: str, index: u64) -> (String)`
1246#[crate::polydat_node(category = RealData)]
1247fn matching_profile_name_at(
1248    group: crate::derive_support::Resolved<crate::derive_support::GroupResolver, DatasetHandle>,
1249    pattern: &str,
1250    index: u64,
1251) -> String {
1252    let group: &TestDataGroup = group_of(&group);
1253    // Same masked sequence `profile_partitions` uses (matching profiles
1254    // with a non-zero base count, canonical order), so `idx_of(p)` from a
1255    // partition resolves to the right tier name.
1256    let tiers = masked_profile_tiers(group, pattern);
1257    if tiers.is_empty() {
1258        String::new()
1259    } else {
1260        tiers[(index as usize) % tiers.len()].0.clone()
1261    }
1262}
1263
1264// =================================================================
1265// Prebuffering — eagerly download dataset facets before workload run
1266// =================================================================
1267
1268source_only_node!(
1269    /// Eagerly download all facets for a dataset profile into the
1270    /// local cache, returning a `DatasetHandle::Group` handle
1271    /// that downstream facet accessors take as their first
1272    /// argument. After this returns, every subsequent facet read
1273    /// served by [`vectordata::TestDataView`] hits the merkle-
1274    /// verified mmap fast path with no further network traffic.
1275    ///
1276    /// Signature: `dataset_prebuffer(source) -> Handle`
1277    ///
1278    /// **Why a handle, not a count.** Returning a value the
1279    /// downstream accessors *consume* makes prebuffer part of
1280    /// the dataflow graph: DCE keeps the chain alive because
1281    /// `vector_at(prebuffered, q)` needs `prebuffered` to be
1282    /// resolvable, which forces evaluation. Bindings such as
1283    /// `init prebuffered = dataset_prebuffer(...)` whose result
1284    /// nothing reads would still be pruned (TODO: rationalise
1285    /// dangling-init dataflow as a separate followup; the
1286    /// established pattern is to thread the handle through).
1287    ///
1288    /// `source` is the canonical `dataset:profile` string. The
1289    /// returned handle is a `Group` handle — accessors that take
1290    /// it route through the same `DatasetHandle::open(...)`
1291    /// resolver path used by other group-aware nodes
1292    /// (`dataset_facets`, `dataset_distance_function`, ...).
1293    ///
1294    /// Errors during prebuffer surface via stderr; the node
1295    /// still returns the group handle so downstream binds don't
1296    /// fail with a "missing value" cascade — the operator's
1297    /// intent is "best effort warm-up", and a workload that
1298    /// wants strict guarantees can wrap this in a `required(...)`
1299    /// predicate or check facet readiness explicitly.
1300    DatasetPrebuffer, "dataset_prebuffer",
1301    out_port = Handle,
1302    eval = |source| do_dataset_prebuffer(source)
1303);
1304
1305/// Implementation behind the `dataset_prebuffer` node — kept
1306/// out of the macro body so we can use `?`/early return
1307/// without fighting the closure-vs-eval-fn return type
1308/// mismatch the `source_only_node!` macro embeds.
1309fn do_dataset_prebuffer(source: &str) -> Value {
1310    // The init-binding contract (SRD 11) means this function is
1311    // expected to fire exactly once per scope activation: Plan B
1312    // pulls the binding on the activation kernel; OpBuilder's
1313    // init_overrides propagate the result to every fiber. The
1314    // PREBUFFER_CACHE below is belt-and-suspenders: it serializes
1315    // anyone who ends up calling here through a non-init path,
1316    // and makes per-source thundering-herd structurally
1317    // impossible regardless of upstream caller behavior.
1318    match PREBUFFER_CACHE.get_or_init(source.to_string(), || {
1319        // Only the first concurrent caller for this source runs
1320        // the inner body — the audit "entered" event reflects
1321        // that single download, not per-caller noise.
1322        crate::library::support::audit::record_prebuffer_entered(source);
1323        do_dataset_prebuffer_inner(source)
1324    }) {
1325        Ok(handle) => Value::handle(handle),
1326        Err(_) => Value::None,
1327    }
1328}
1329
1330/// Inner body of [`do_dataset_prebuffer`]. Runs **at most once
1331/// per (source, process)** under the [`PREBUFFER_CACHE`] OnceLock.
1332fn do_dataset_prebuffer_inner(source: &str) -> Result<Arc<DatasetHandle>, String> {
1333    // Return a Group handle in every exit (success or error) so
1334    // the downstream `*_at(prebuffered, q)` accessors can resolve.
1335    // Failed prebuffer still hands back the group handle — the
1336    // accessors will then HTTP-fall-through, and the operator
1337    // sees the prebuffer error in the audit log.
1338    let group = match load_dataset_group(source) {
1339        Ok(g) => g,
1340        Err(e) => {
1341            let msg = format!("dataset_prebuffer: cannot resolve '{source}': {e}");
1342            crate::library::support::audit::error(&msg);
1343            // No group → no handle to hand back. Sticky error in
1344            // the cache; downstream accessors will produce a
1345            // diagnostic when they fail to downcast.
1346            return Err(msg);
1347        }
1348    };
1349    let group_for_handle = group.clone();
1350    let (_, profile) = parse_source_specifier(source);
1351    let view = match group.profile(profile) {
1352        Some(v) => v,
1353        None => {
1354            crate::library::support::audit::error(&format!(
1355                "dataset_prebuffer: profile '{profile}' not found in '{source}'"
1356            ));
1357            return Ok(Arc::new(DatasetHandle::Prebuffered {
1358                _group: group_for_handle,
1359                source: source.to_string(),
1360            }));
1361        }
1362    };
1363    // vectordata's default `prebuffer_all_with_progress`
1364    // walks the manifest and calls `FacetStorage::prebuffer`
1365    // per facet — works for both record-shaped (xvec) and
1366    // scalar (typed) facets after the storage-transport
1367    // refactor in vectordata 1.0.0.
1368    //
1369    // Audit instrumentation: log every facet the prebuffer
1370    // *covers* with its key (`source:profile/facet`). Compared
1371    // against the `vectordata: opened …` lines emitted by
1372    // [`load_uniform_facet`] / [`GenericFacetDataset::load`],
1373    // this surfaces any facet the workload reads at cycle time
1374    // that prebuffer did NOT pull — the typical cause of a
1375    // "still hitting HTTP after prebuffer" symptom.
1376    // Manual facet walk so we can hook the per-chunk
1377    // `DownloadProgress` callback (vectordata's
1378    // `view.prebuffer_all_with_progress` only fires its outer cb
1379    // once per facet *completion*; the per-chunk cb is exposed
1380    // via `FacetStorage::prebuffer_with_progress`). Every facet's
1381    // chunk-level progress is throttled per facet: ~1 Hz for the first
1382    // 10s of its download (fine detail while it ramps), then one line
1383    // per 10s for the long tail so a multi-gigabyte facet doesn't spew
1384    // thousands of lines into session.log; a final per-facet `covered`
1385    // line lands when the download finishes.
1386    let mut facet_count: u64 = 0;
1387    for (name, _descriptor) in view.facet_manifest() {
1388        // Skip facets with unrecognised element types (vectordata's
1389        // own default impl skips these — they're not data facets the
1390        // typed reader would touch).
1391        if view.facet_element_type(&name).is_err() {
1392            continue;
1393        }
1394
1395        let storage = match run_blocking_io(|| view.open_facet_storage(&name)) {
1396            Ok(s) => s,
1397            Err(e) => {
1398                crate::library::support::audit::warn(&format!(
1399                    "dataset_prebuffer: open '{name}' for prebuffer failed: {e}"
1400                ));
1401                continue;
1402            }
1403        };
1404
1405        // Inline the closure at the call site so Rust's type
1406        // inference can read `&DownloadProgress` straight from
1407        // the trait bound on `prebuffer_with_progress`.
1408        // The `DownloadProgress` type is `pub(crate)` upstream
1409        // (vectordata 1.0.2) so we can't name it ourselves.
1410        let prebuf_source = source.to_string();
1411        let prebuf_profile = profile.to_string();
1412        let prebuf_facet = name.clone();
1413        let mut last_done: u64 = 0;
1414        let facet_start = std::time::Instant::now();
1415        let mut last_log_at = facet_start;
1416        // `prebuffer_with_progress` drives `reqwest::blocking`,
1417        // which spins up a private tokio runtime per request.
1418        // Without parking the outer worker via `block_in_place`,
1419        // dropping that inner runtime inside an async context
1420        // panics with "Cannot drop a runtime in a context where
1421        // blocking is not allowed". Same pattern as
1422        // `load_dataset_group` and `load_uniform_facet`.
1423        let prebuf_result = run_blocking_io(|| {
1424            storage.prebuffer_with_progress(|p| {
1425                // Per-facet throttle: ~1 Hz for the first 10s of this
1426                // facet's download, then once per 10s, so a gigabyte-sized
1427                // facet doesn't spew thousands of progress lines.
1428                let now = std::time::Instant::now();
1429                let interval =
1430                    if now.duration_since(facet_start) < std::time::Duration::from_secs(10) {
1431                        std::time::Duration::from_secs(1)
1432                    } else {
1433                        std::time::Duration::from_secs(10)
1434                    };
1435                let since_last = now.duration_since(last_log_at);
1436                if since_last < interval {
1437                    return;
1438                }
1439                last_log_at = now;
1440                let total_b = p.total_bytes();
1441                let done_b = p.downloaded_bytes();
1442                let total_c = p.total_chunks();
1443                let done_c = p.completed_chunks();
1444                let pct = p.fraction() * 100.0;
1445                // Throughput over the actual gap between log lines, so the
1446                // rate reads correctly whichever interval is in force.
1447                let delta_mb = (done_b.saturating_sub(last_done)) as f64 / (1024.0 * 1024.0);
1448                let rate_mb_s = delta_mb / since_last.as_secs_f64().max(0.001);
1449                last_done = done_b;
1450                crate::library::support::audit::info(&format!(
1451                    "prebuffer: progress {prebuf_source}:{prebuf_profile}/{prebuf_facet} \
1452                 {pct:5.1}% ({done_c}/{total_c} chunks, \
1453                 {done_mb:.1}/{total_mb:.1} MB, {rate_mb_s:.1} MB/s)",
1454                    done_mb = done_b as f64 / (1024.0 * 1024.0),
1455                    total_mb = total_b as f64 / (1024.0 * 1024.0),
1456                ));
1457            })
1458        });
1459        if let Err(e) = prebuf_result {
1460            crate::library::support::audit::warn(&format!(
1461                "dataset_prebuffer: download error for '{source}' facet '{name}': {e}"
1462            ));
1463            continue;
1464        }
1465        facet_count = facet_count.saturating_add(1);
1466        crate::library::support::audit::record_prebuffered(source, profile, &name);
1467    }
1468    crate::library::support::audit::log_prebuffer_summary(source, profile, facet_count);
1469    Ok(Arc::new(DatasetHandle::Prebuffered {
1470        _group: group_for_handle,
1471        source: source.to_string(),
1472    }))
1473}
1474
1475// =================================================================
1476// Generic facet access — type-aware scalar/vector readers
1477// =================================================================
1478
1479/// A type-erased facet reader that stores values as i64.
1480/// Uses `generic_view().open_facet_typed::<i64>()` which handles all
1481/// element types (u8, i32, etc.), caching, and local/remote access.
1482pub(crate) struct GenericFacetDataset {
1483    reader: vectordata::typed_access::TypedReader<i64>,
1484    count: usize,
1485}
1486
1487impl GenericFacetDataset {
1488    fn load(source: &str, profile: &str, facet: &str) -> Result<Arc<Self>, String> {
1489        let key = (source.to_string(), profile.to_string(), facet.to_string());
1490        let any = FACET_CACHE.get_or_init(key, || {
1491            let group = load_dataset_group(source)?;
1492            let gv = group
1493                .generic_view(profile)
1494                .ok_or_else(|| format!("profile '{profile}' not found in '{source}'"))?;
1495            crate::library::support::audit::record_opened(source, profile, facet, "generic-typed");
1496            let reader = run_blocking_io(|| gv.open_facet_typed::<i64>(facet))
1497                .map_err(|e| format!("failed to open {facet} from '{source}:{profile}': {e}"))?;
1498            let count = reader.count();
1499            let arc: Arc<Self> = Arc::new(Self { reader, count });
1500            Ok(arc as Arc<dyn std::any::Any + Send + Sync>)
1501        })?;
1502        any.downcast::<Self>()
1503            .map_err(|_| format!("facet cache type mismatch for '{source}:{profile}/{facet}'"))
1504    }
1505
1506    fn get_scalar(&self, index: usize) -> i64 {
1507        if self.count == 0 {
1508            return 0;
1509        }
1510        self.reader.get_value(index % self.count).unwrap_or(0)
1511    }
1512
1513    fn format_scalar(&self, index: usize) -> String {
1514        self.get_scalar(index).to_string()
1515    }
1516}
1517
1518// Generic-facet readers (typed scalar). Take a Generic handle and
1519// read the i64-cast value at the requested index.
1520
1521fn generic_str_at(h: &DatasetHandle, idx: usize) -> Value {
1522    match h {
1523        DatasetHandle::Generic(d) => Value::Str(d.format_scalar(idx).into()),
1524        _ => Value::Str(String::new().into()),
1525    }
1526}
1527
1528handle_indexed_node!(
1529    /// Access a metadata value per base ordinal. Expects a Generic
1530    /// handle opened against the `metadata_content` facet.
1531    ///
1532    /// Signature: `metadata_value_at(handle, index: u64) -> (String)`
1533    MetadataValueAt, "metadata_value_at", Str, facet = "metadata_content", eval = generic_str_at
1534);
1535
1536handle_indexed_node!(
1537    /// Access a predicate value per query ordinal. Expects a Generic
1538    /// handle opened against the `metadata_predicates` facet.
1539    ///
1540    /// Signature: `predicate_value_at(handle, index: u64) -> (String)`
1541    PredicateValueAt, "predicate_value_at", Str, facet = "metadata_predicates", eval = generic_str_at
1542);
1543
1544handle_metadata_node!(
1545    /// Count of metadata content records. Expects a Generic handle.
1546    ///
1547    /// Signature: `metadata_content_count(handle) -> (u64)`
1548    MetadataContentCount, "metadata_content_count", U64,
1549    eval = |h: &DatasetHandle| match h {
1550        DatasetHandle::Generic(d) => Value::U64(d.count as u64),
1551        _ => Value::U64(0),
1552    }
1553);
1554
1555// ---------------------------------------------------------------------------
1556// Signature declarations for the DSL registry
1557// ---------------------------------------------------------------------------
1558
1559use crate::ast::SlotType;
1560use crate::dsl::registry::{Arity, DefaultResolver, FuncCategory, FuncSig, ParamSpec};
1561
1562// Macros to keep the bulk of the registry compact and consistent.
1563// Per SRD 53 §"Source-string call-site sugar": each handle-taking
1564// accessor declares a `default_resolver` so the binding compiler
1565// can promote a string source into the right resolver call.
1566
1567macro_rules! sig_handle_indexed {
1568    ($name:literal, $resolver:expr, $desc:literal, $help:literal) => {
1569        FuncSig {
1570            name: $name,
1571            category: FuncCategory::RealData,
1572            outputs: 1,
1573            description: $desc,
1574            help: $help,
1575            identity: None,
1576            variadic_ctor: None,
1577            params: &[
1578                ParamSpec {
1579                    name: "handle",
1580                    slot_type: SlotType::Wire,
1581                    required: true,
1582                    example: "base",
1583                    constraint: None,
1584                },
1585                ParamSpec {
1586                    name: "index",
1587                    slot_type: SlotType::Wire,
1588                    required: true,
1589                    example: "cycle",
1590                    constraint: None,
1591                },
1592            ],
1593            arity: Arity::Fixed,
1594            commutativity: crate::ast::Commutativity::Positional,
1595            default_resolver: Some($resolver),
1596            output_type: crate::dsl::registry::OutputType::Fixed,
1597            // Hand registration: no static return-port declaration;
1598            // type inference falls back to the name heuristic.
1599            output_port: None,
1600        }
1601    };
1602}
1603
1604macro_rules! sig_handle_metadata {
1605    ($name:literal, $resolver:expr, $desc:literal, $help:literal) => {
1606        FuncSig {
1607            name: $name,
1608            category: FuncCategory::RealData,
1609            outputs: 1,
1610            description: $desc,
1611            help: $help,
1612            identity: None,
1613            variadic_ctor: None,
1614            params: &[ParamSpec {
1615                name: "handle",
1616                slot_type: SlotType::Wire,
1617                required: true,
1618                example: "base",
1619                constraint: None,
1620            }],
1621            arity: Arity::Fixed,
1622            commutativity: crate::ast::Commutativity::Positional,
1623            default_resolver: Some($resolver),
1624            output_type: crate::dsl::registry::OutputType::Fixed,
1625            // Hand registration: no static return-port declaration;
1626            // type inference falls back to the name heuristic.
1627            output_port: None,
1628        }
1629    };
1630}
1631
1632/// Signatures for vector dataset access nodes (feature-gated).
1633///
1634/// `dataset_open` and `dataset_group_open` register themselves via the
1635/// `#[polydat_node]` macro's own `NodeRegistration` channel.
1636pub fn signatures() -> &'static [FuncSig] {
1637    use FuncCategory as C;
1638    &[
1639        // ===== Per-cycle facet accessors (typed-vector outputs) =====
1640        sig_handle_indexed!(
1641            "vector_at",
1642            DefaultResolver::Facet("base"),
1643            "access f32 vector by index",
1644            "Read an f32 vector from a facet handle as a typed VecF32.\nAuto-promotes a string source via dataset_open(_,\"base\").\nExample: vector_at(base, cycle)"
1645        ),
1646        sig_handle_indexed!(
1647            "query_vector_at",
1648            DefaultResolver::Facet("query"),
1649            "access query vector by index",
1650            "Alias for vector_at over a query-facet handle.\nAuto-promotes a string source via dataset_open(_,\"query\")."
1651        ),
1652        sig_handle_indexed!(
1653            "neighbor_indices_at",
1654            DefaultResolver::Facet("neighbor_indices"),
1655            "ground-truth neighbor indices for a query",
1656            "Read ground-truth k-nearest neighbor indices for a query as a\ntyped VecI32. Auto-promotes a string source via\ndataset_open(_,\"neighbor_indices\")."
1657        ),
1658        sig_handle_indexed!(
1659            "neighbor_distances_at",
1660            DefaultResolver::Facet("neighbor_distances"),
1661            "ground-truth neighbor distances for a query",
1662            "Read ground-truth distances for a query's k-nearest neighbors\nas a typed VecF32."
1663        ),
1664        sig_handle_indexed!(
1665            "filtered_neighbor_indices_at",
1666            DefaultResolver::Facet("filtered_neighbor_indices"),
1667            "filtered ground-truth neighbor indices",
1668            "Read filtered ground-truth indices for a query as a typed VecI32.\nUsed for filtered-ANN recall verification."
1669        ),
1670        sig_handle_indexed!(
1671            "filtered_neighbor_distances_at",
1672            DefaultResolver::Facet("filtered_neighbor_distances"),
1673            "filtered ground-truth neighbor distances",
1674            "Read filtered ground-truth distances for a query as a typed VecF32."
1675        ),
1676        sig_handle_indexed!(
1677            "metadata_results_len_at",
1678            DefaultResolver::Facet("metadata_results"),
1679            "length of metadata indices for a query",
1680            "Return the per-record matching-base count for a query without\nloading the full index list (reads only the 4-byte header)."
1681        ),
1682        sig_handle_indexed!(
1683            "metadata_results_at",
1684            DefaultResolver::Facet("metadata_results"),
1685            "matching base ordinals for a query predicate",
1686            "Variable-length list of base vector ordinals matching a query's\npredicate."
1687        ),
1688        sig_handle_indexed!(
1689            "metadata_value_at",
1690            DefaultResolver::Facet("metadata_content"),
1691            "scalar metadata value per base vector",
1692            "Read a metadata value for a base vector by ordinal.\nReads from the metadata_content facet."
1693        ),
1694        sig_handle_indexed!(
1695            "predicate_value_at",
1696            DefaultResolver::Facet("metadata_predicates"),
1697            "scalar predicate value per query",
1698            "Read a predicate value for a query by ordinal.\nReads from the metadata_predicates facet."
1699        ),
1700        // ===== Per-handle metadata =====
1701        sig_handle_metadata!(
1702            "vector_dim",
1703            DefaultResolver::Facet("base"),
1704            "vector dimensionality of a facet handle",
1705            "Return the per-record element count (dimension) of a vector facet."
1706        ),
1707        sig_handle_metadata!(
1708            "vector_count",
1709            DefaultResolver::Facet("base"),
1710            "record count of a facet handle",
1711            "Return the number of records in a facet handle (base vectors,\nquery vectors, ...). Auto-promotes a string source via\ndataset_open(_,\"base\")."
1712        ),
1713        sig_handle_metadata!(
1714            "query_count",
1715            DefaultResolver::Facet("query"),
1716            "record count of a query-facet handle",
1717            "Same as vector_count but defaults to dataset_open(_,\"query\")\nfor string sources."
1718        ),
1719        sig_handle_metadata!(
1720            "neighbor_count",
1721            DefaultResolver::Facet("neighbor_indices"),
1722            "ground-truth neighbors per query (maxk)",
1723            "Return the per-record neighbor count (k) of a neighbor-indices handle."
1724        ),
1725        sig_handle_metadata!(
1726            "metadata_results_count",
1727            DefaultResolver::Facet("metadata_results"),
1728            "number of predicate result sets",
1729            "Return the record count of a metadata-indices handle."
1730        ),
1731        sig_handle_metadata!(
1732            "metadata_content_count",
1733            DefaultResolver::Facet("metadata_content"),
1734            "number of metadata content records",
1735            "Return the record count of a metadata-content handle."
1736        ),
1737        // ===== Group-level (Group handle) =====
1738        sig_handle_metadata!(
1739            "dataset_distance_function",
1740            DefaultResolver::Group,
1741            "dataset distance/similarity function name",
1742            "Return the distance function declared in the dataset metadata\n('COSINE','EUCLIDEAN','DOT_PRODUCT','MANHATTAN'). Group-level."
1743        ),
1744        sig_handle_metadata!(
1745            "dataset_facets",
1746            DefaultResolver::Group,
1747            "list available facets in default profile",
1748            "Comma-separated facet names available in the group's default profile."
1749        ),
1750        sig_handle_metadata!(
1751            "dataset_profile_count",
1752            DefaultResolver::Group,
1753            "total number of profiles in a dataset",
1754            "Number of profiles defined in the dataset group."
1755        ),
1756        sig_handle_metadata!(
1757            "dataset_profile_names",
1758            DefaultResolver::Group,
1759            "comma-separated list of profile names",
1760            "All profile names in canonical sort order (by base_count)."
1761        ),
1762        // `matching_profiles`, `dataset_profile_name_at`,
1763        // `profile_base_count`, and `profile_facets` are now
1764        // `#[polydat_node]`-emitted; they register their FuncSig
1765        // via the macro's NodeRegistration entry. Their
1766        // `Resolved<GroupResolver, TestDataGroup>` arg supplies
1767        // the `default_resolver: Some(DefaultResolver::Group)`
1768        // value to the emitted FuncSig automatically (Wire-trait
1769        // RESOLVER projection — SRD-80b).
1770
1771        // ===== Side-effect resolver =====
1772        FuncSig {
1773            name: "dataset_prebuffer",
1774            category: C::RealData,
1775            outputs: 1,
1776            description: "eagerly download dataset facets to local cache",
1777            help: "Downloads all facets for a dataset to the local cache. Returns 0\n(side-effect resolver). Subsequent loads use fast local mmap access.\nKept on a string source — typically called once per workload at\ninit time.\nExample: const _pb := dataset_prebuffer(\"example\")",
1778            identity: None,
1779            variadic_ctor: None,
1780            params: &[ParamSpec {
1781                name: "source",
1782                slot_type: SlotType::Wire,
1783                required: true,
1784                example: "\"test\"",
1785                constraint: None,
1786            }],
1787            arity: Arity::Fixed,
1788            commutativity: crate::ast::Commutativity::Positional,
1789            default_resolver: None,
1790            output_type: crate::dsl::registry::OutputType::Fixed,
1791            // Hand registration: no static return-port declaration;
1792            // type inference falls back to the name heuristic.
1793            output_port: None,
1794        },
1795    ]
1796}
1797
1798/// Try to build a vector dataset node from a function name and const args.
1799///
1800/// Returns `None` if the name is not handled by this module.
1801/// All functions in this module are feature-gated on `vectordata`.
1802#[cfg(feature = "vectordata")]
1803pub(crate) fn build_node(
1804    name: &str,
1805    _wires: &[crate::compile::assembly::WireRef],
1806    _wire_types: &[crate::ast::PortType],
1807    _consts: &[crate::dsl::factory::ConstArg],
1808) -> Option<Result<Box<dyn crate::ast::PolydatNode>, String>> {
1809    // Every dataset function in this module now takes its
1810    // `source` (and any other previously-const string params)
1811    // as a Wire input, so `consts` is unused — the spec arrives
1812    // through `inputs[i]` at eval time. Literal-string args
1813    // are auto-lifted to anonymous `ConstStr` wire nodes by the
1814    // binding compiler; non-literal args (e.g. `printf` from a
1815    // string-interpolated source spec) wire directly.
1816    match name {
1817        // `dataset_open` and `dataset_group_open` register through
1818        // their `#[polydat_node]`-emitted NodeRegistration entries.
1819        "vector_at" => Some(Ok(
1820            Box::new(VectorAt::new()) as Box<dyn crate::ast::PolydatNode>
1821        )),
1822        "query_vector_at" => Some(Ok(
1823            Box::new(QueryVectorAt::new()) as Box<dyn crate::ast::PolydatNode>
1824        )),
1825        "neighbor_indices_at" => Some(Ok(
1826            Box::new(NeighborIndicesAt::new()) as Box<dyn crate::ast::PolydatNode>
1827        )),
1828        "neighbor_distances_at" => Some(Ok(
1829            Box::new(NeighborDistancesAt::new()) as Box<dyn crate::ast::PolydatNode>
1830        )),
1831        "filtered_neighbor_indices_at" => Some(Ok(
1832            Box::new(FilteredNeighborIndicesAt::new()) as Box<dyn crate::ast::PolydatNode>
1833        )),
1834        "filtered_neighbor_distances_at" => Some(Ok(
1835            Box::new(FilteredNeighborDistancesAt::new()) as Box<dyn crate::ast::PolydatNode>
1836        )),
1837        "dataset_distance_function" => Some(Ok(
1838            Box::new(DatasetDistanceFunction::new()) as Box<dyn crate::ast::PolydatNode>
1839        )),
1840        "vector_dim" => Some(Ok(
1841            Box::new(VectorDim::new()) as Box<dyn crate::ast::PolydatNode>
1842        )),
1843        "vector_count" => Some(Ok(
1844            Box::new(VectorCount::new()) as Box<dyn crate::ast::PolydatNode>
1845        )),
1846        "query_count" => Some(Ok(
1847            Box::new(QueryCount::new()) as Box<dyn crate::ast::PolydatNode>
1848        )),
1849        "neighbor_count" => Some(Ok(
1850            Box::new(NeighborCount::new()) as Box<dyn crate::ast::PolydatNode>
1851        )),
1852        "metadata_results_len_at" => Some(Ok(
1853            Box::new(MetadataResultsLenAt::new()) as Box<dyn crate::ast::PolydatNode>
1854        )),
1855        "metadata_results_at" => Some(Ok(
1856            Box::new(MetadataResultsAt::new()) as Box<dyn crate::ast::PolydatNode>
1857        )),
1858        "metadata_results_count" => Some(Ok(
1859            Box::new(MetadataResultsCount::new()) as Box<dyn crate::ast::PolydatNode>
1860        )),
1861        "dataset_facets" => Some(Ok(
1862            Box::new(DatasetFacets::new()) as Box<dyn crate::ast::PolydatNode>
1863        )),
1864        "dataset_profile_count" => Some(Ok(
1865            Box::new(DatasetProfileCount::new()) as Box<dyn crate::ast::PolydatNode>
1866        )),
1867        "dataset_profile_names" => Some(Ok(
1868            Box::new(DatasetProfileNames::new()) as Box<dyn crate::ast::PolydatNode>
1869        )),
1870        // `matching_profiles`, `dataset_profile_name_at`,
1871        // `profile_base_count`, `profile_facets` register
1872        // through the `#[polydat_node]`-emitted NodeRegistration.
1873        "dataset_prebuffer" => Some(Ok(
1874            Box::new(DatasetPrebuffer::new()) as Box<dyn crate::ast::PolydatNode>
1875        )),
1876        "metadata_value_at" => Some(Ok(
1877            Box::new(MetadataValueAt::new()) as Box<dyn crate::ast::PolydatNode>
1878        )),
1879        "predicate_value_at" => Some(Ok(
1880            Box::new(PredicateValueAt::new()) as Box<dyn crate::ast::PolydatNode>
1881        )),
1882        "metadata_content_count" => Some(Ok(
1883            Box::new(MetadataContentCount::new()) as Box<dyn crate::ast::PolydatNode>
1884        )),
1885        _ => None,
1886    }
1887}
1888
1889#[cfg(feature = "vectordata")]
1890crate::register_nodes!(signatures, build_node);
1891
1892// =========================================================================
1893// Cursor-sugar handlers (SRD 18 §"Source-driven workloads")
1894// =========================================================================
1895//
1896// Three sugar forms recognized by this module — none of them
1897// known to the core compiler, which dispatches generically
1898// through `dsl::cursor_sugar`. Each desugars to a synthetic
1899// `range(0, vector_count|query_count(...))` constructor plus
1900// auxiliary bindings:
1901//
1902//   - `__<cursor>_prebuffer := dataset_prebuffer("ds:profile")`
1903//     loaded once at init time so cycle-time accessor calls hit
1904//     prefetched memory.
1905//   - `<cursor>__vector := vector_at("ds:profile", <cursor>__ordinal)`
1906//     (or `query_vector_at` for the query
1907//     facet) — published as the cursor's `vector` projection so
1908//     workloads can reference `<cursor>.vector`.
1909//
1910// Forms:
1911//   - `vectordata_source(dataset, profile, facet)` — explicit facet
1912//   - `vectordata_base(dataset, profile)`           — facet = "base"
1913//   - `vectordata_query(dataset, profile)`          — facet = "query"
1914//
1915// Facet-specific projections like `metadata` / `ground_truth` /
1916// `predicate` stay explicit (the user writes
1917// `meta := metadata_value_at(<cursor>.ordinal, "ds:profile")` by
1918// hand) because their existence is dataset-conditional — not
1919// every dataset declares a metadata column or a predicate facet.
1920
1921#[cfg(feature = "vectordata")]
1922fn vectordata_sugar(
1923    source_name: &str,
1924    constructor: &crate::dsl::ast::Expr,
1925) -> Result<Option<crate::dsl::cursor_sugar::CursorSugar>, String> {
1926    use crate::dsl::ast::{Arg, CallExpr, Expr};
1927    use crate::dsl::compile::positional_str_lit;
1928    use crate::dsl::cursor_sugar::{AuxBinding, CursorSugar};
1929
1930    let Expr::Call(call) = constructor else {
1931        return Ok(None);
1932    };
1933
1934    let (dataset, profile, facet) = match call.func.as_str() {
1935        "vectordata_source" => {
1936            let d = positional_str_lit(call.args.first()).ok_or_else(|| format!(
1937                "cursor '{source_name}': vectordata_source(dataset, profile, facet) — first arg must be a string literal"
1938            ))?;
1939            let p = positional_str_lit(call.args.get(1)).ok_or_else(|| format!(
1940                "cursor '{source_name}': vectordata_source(dataset, profile, facet) — second arg must be a string literal"
1941            ))?;
1942            let f = positional_str_lit(call.args.get(2)).ok_or_else(|| format!(
1943                "cursor '{source_name}': vectordata_source(dataset, profile, facet) — third arg must be a string literal (\"base\" or \"query\")"
1944            ))?;
1945            (d, p, f)
1946        }
1947        "vectordata_base" | "vectordata_query" => {
1948            let f = call.func.strip_prefix("vectordata_").unwrap().to_string();
1949            let d = positional_str_lit(call.args.first()).ok_or_else(|| format!(
1950                "cursor '{source_name}': {}(dataset, profile) — first arg must be a string literal",
1951                call.func,
1952            ))?;
1953            let p = positional_str_lit(call.args.get(1)).ok_or_else(|| format!(
1954                "cursor '{source_name}': {}(dataset, profile) — second arg must be a string literal",
1955                call.func,
1956            ))?;
1957            (d, p, f)
1958        }
1959        _ => return Ok(None),
1960    };
1961
1962    if facet != "base" && facet != "query" {
1963        return Err(format!(
1964            "cursor '{source_name}': vectordata facet must be \"base\" or \"query\", got \"{facet}\""
1965        ));
1966    }
1967    let (count_func, vector_func) = match facet.as_str() {
1968        "base" => ("vector_count", "vector_at"),
1969        "query" => ("query_count", "query_vector_at"),
1970        _ => unreachable!(),
1971    };
1972
1973    let combined = format!("{dataset}:{profile}");
1974    let span = call.span;
1975    let lit = |s: String| Expr::StringLit(s, span);
1976    let positional = |e: Expr| Arg::Positional(e);
1977
1978    let effective_constructor = Expr::Call(CallExpr {
1979        func: "range".into(),
1980        args: vec![
1981            positional(Expr::IntLit(0, span)),
1982            positional(Expr::Call(CallExpr {
1983                func: count_func.into(),
1984                args: vec![positional(lit(combined.clone()))],
1985                span,
1986            })),
1987        ],
1988        span,
1989    });
1990
1991    let prebuffer_binding = AuxBinding {
1992        name: format!("__{source_name}_prebuffer"),
1993        value: Expr::Call(CallExpr {
1994            func: "dataset_prebuffer".into(),
1995            args: vec![positional(lit(combined.clone()))],
1996            span,
1997        }),
1998        projection: None,
1999    };
2000
2001    // Cursor sugar emits the call with the new (handle, index) order.
2002    // The combined source string auto-promotes to the right facet
2003    // handle via the binding compiler's call-site sugar (SRD 53).
2004    let vector_binding = AuxBinding {
2005        name: format!("{source_name}__vector"),
2006        value: Expr::Call(CallExpr {
2007            func: vector_func.into(),
2008            args: vec![
2009                positional(lit(combined)),
2010                positional(Expr::Ident(format!("{source_name}__ordinal"), span)),
2011            ],
2012            span,
2013        }),
2014        projection: Some(("vector".into(), crate::ast::PortType::VecF32)),
2015    };
2016
2017    Ok(Some(CursorSugar {
2018        effective_constructor,
2019        aux_bindings: vec![prebuffer_binding, vector_binding],
2020    }))
2021}
2022
2023#[cfg(feature = "vectordata")]
2024inventory::submit! {
2025    crate::dsl::cursor_sugar::CursorSugarRegistration {
2026        handler: vectordata_sugar,
2027        name: "vectordata",
2028    }
2029}