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}