Skip to main content

qualia_core_db/sparql_library/immersive/
functions.rs

1//! QISP typed function registry (Phase 4) — the concrete descriptor table that
2//! makes the §4.2 "no untyped `u64 -> u64` public function contract" real.
3//!
4//! Every QISP extension function (`qispf:`) is registered here with its typed
5//! [`ImmersiveFunctionDescriptor`]: argument kinds, result kind, execution class,
6//! determinism, exactness, and I/O byte budgets. The SPARQL evaluator can look a
7//! function up by the `q_hash` of its IRI (the `Function::Custom(iri_hash)` path)
8//! and apply **admission control** — reject an inline `FILTER`/`BIND` use of an
9//! async/table-producing function, or an over-budget call — *before* dispatch,
10//! instead of discovering the problem mid-execution (plan §4.2, §6.1).
11//!
12//! Where GeoSPARQL already defines an operation for 2D geometry, the QISP function
13//! **defers** to the `geof:` name (plan §4.1 "GeoSPARQL names and semantics take
14//! precedence"); the QISP descriptor exists for the mesh / volumetric / tensor /
15//! higher-dimensional cases GeoSPARQL does not define. The deference is recorded in
16//! [`FunctionEntry::defers_to`] so a planner can route a 2D call to the GeoSPARQL
17//! engine and a 3D/mesh call to the native kernel.
18//!
19//! This is the typed *registry*; binding a descriptor to a live geometry/tensor
20//! kernel over resolved [`DenseAssetRef`](super::asset_registry::DenseAssetRef)s is
21//! the next Phase-4 increment (it needs the asset registry populated with real
22//! ingested geometry). The registry + admission contract land first so the boundary
23//! is typed and honest.
24
25use super::value::{
26    ExactnessClass as E, ExecutionClass as X, ImmersiveFunctionDescriptor as Desc,
27    ImmersiveValueKind as K, QispError,
28};
29use crate::tensor::Tensor10D;
30
31/// One bounded nearest-neighbour result. `index` identifies the input slice slot;
32/// `distance` is the canonical [`Tensor10D::full_distance`] value.
33#[repr(C)]
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct TensorNeighbor {
36    pub index: u32,
37    pub distance: f32,
38}
39
40/// Exact, allocation-free Tensor10D distance using the resident substrate's
41/// canonical topology-aware metric.
42pub fn tensor_distance(left: &Tensor10D, right: &Tensor10D) -> Result<f32, QispError> {
43    let distance = left.full_distance(right);
44    if distance.is_finite() {
45        Ok(distance)
46    } else {
47        Err(QispError::ProfileMismatch)
48    }
49}
50
51/// Exact radius predicate. Invalid/non-finite radii fail as profile errors rather
52/// than silently producing `false`.
53pub fn tensor_within(left: &Tensor10D, right: &Tensor10D, radius: f32) -> Result<bool, QispError> {
54    if !radius.is_finite() || radius < 0.0 {
55        return Err(QispError::ProfileMismatch);
56    }
57    Ok(tensor_distance(left, right)? <= radius)
58}
59
60/// Deterministic, allocation-free bounded k-nearest-neighbour scan.
61///
62/// Results are ordered by `(distance, input_index)`, making ties stable across
63/// runs. The caller controls both the maximum result count (`k`) and storage. An
64/// undersized output is an explicit budget error; no partial result is exposed.
65pub fn tensor_knn_into(
66    query: &Tensor10D,
67    candidates: &[Tensor10D],
68    k: usize,
69    out: &mut [TensorNeighbor],
70) -> Result<usize, QispError> {
71    if k > out.len() || k > u32::MAX as usize || candidates.len() > u32::MAX as usize {
72        return Err(QispError::BudgetExceeded);
73    }
74    if k == 0 || candidates.is_empty() {
75        return Ok(0);
76    }
77    let wanted = k.min(candidates.len());
78    for (index, candidate) in candidates.iter().enumerate() {
79        let distance = tensor_distance(query, candidate)?;
80        let item = TensorNeighbor {
81            index: index as u32,
82            distance,
83        };
84        let populated = index.min(wanted);
85        let mut pos = populated;
86        while pos > 0 {
87            let prev = out[pos - 1];
88            if prev.distance < distance || (prev.distance == distance && prev.index < item.index) {
89                break;
90            }
91            pos -= 1;
92        }
93        if pos < wanted {
94            let upper = populated.min(wanted - 1);
95            for slot in (pos..upper).rev() {
96                out[slot + 1] = out[slot];
97            }
98            out[pos] = item;
99        }
100    }
101    Ok(wanted)
102}
103
104/// One registered QISP function: its provisional IRI, typed descriptor, and the
105/// GeoSPARQL name it defers to for the 2D case (if any).
106#[derive(Debug, Clone, Copy)]
107pub struct FunctionEntry {
108    /// Absolute provisional IRI (`qispf:` namespace).
109    pub iri: &'static str,
110    /// Typed descriptor (carries `iri_hash = q_hash(iri)`).
111    pub descriptor: Desc,
112    /// The `geof:` function this defers to for 2D geometry, or `None` when QISP
113    /// owns the operation (mesh / tensor / higher-dimensional).
114    pub defers_to: Option<&'static str>,
115}
116
117/// Const constructor so the table below can be a `const`.
118const fn entry(
119    iri: &'static str,
120    args: [K; 4],
121    arg_count: u8,
122    result: K,
123    execution: X,
124    deterministic: bool,
125    exactness: E,
126    max_input_bytes: u32,
127    max_output_bytes: u32,
128    defers_to: Option<&'static str>,
129) -> FunctionEntry {
130    FunctionEntry {
131        iri,
132        descriptor: Desc::new(
133            crate::q_hash(iri),
134            args,
135            arg_count,
136            result,
137            execution,
138            deterministic,
139            exactness,
140            max_input_bytes,
141            max_output_bytes,
142        ),
143        defers_to,
144    }
145}
146
147// Byte-budget shorthands (admission control; reference the geometry capability
148// manifests' scale, not new limits).
149const MB: u32 = 1 << 20;
150
151/// The registered QISP function set (plan §4.1). Ordered by family. A predicate over
152/// two geometries takes `(GeometryRef, GeometryRef)`; the trailing exactness selector
153/// in the surface syntax (e.g. `qisp:Exact`) is a per-call modifier, not a descriptor
154/// argument — the descriptor's `exactness` is the registered default.
155pub const FUNCTIONS: &[FunctionEntry] = &[
156    // ── Topological (hot predicate after coarse filter) ──────────────────────
157    entry(
158        "https://webizen.org/immersive/function/0.1#intersects",
159        [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
160        2,
161        K::Boolean,
162        X::HotZeroHeap,
163        true,
164        E::Exact,
165        MB,
166        0,
167        Some("http://www.opengis.net/def/function/geosparql/sfIntersects"),
168    ),
169    entry(
170        "https://webizen.org/immersive/function/0.1#contains",
171        [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
172        2,
173        K::Boolean,
174        X::HotZeroHeap,
175        true,
176        E::Exact,
177        MB,
178        0,
179        Some("http://www.opengis.net/def/function/geosparql/sfContains"),
180    ),
181    entry(
182        "https://webizen.org/immersive/function/0.1#touches",
183        [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
184        2,
185        K::Boolean,
186        X::HotZeroHeap,
187        true,
188        E::Exact,
189        MB,
190        0,
191        Some("http://www.opengis.net/def/function/geosparql/sfTouches"),
192    ),
193    // ── Proximity (hot query, caller-buffered) ───────────────────────────────
194    entry(
195        "https://webizen.org/immersive/function/0.1#distance",
196        [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
197        2,
198        K::Quantity,
199        X::HotZeroHeap,
200        true,
201        E::Exact,
202        MB,
203        0,
204        Some("http://www.opengis.net/def/function/geosparql/distance"),
205    ),
206    entry(
207        "https://webizen.org/immersive/function/0.1#withinDistance",
208        [K::GeometryRef, K::GeometryRef, K::Scalar, K::Boolean],
209        3,
210        K::Boolean,
211        X::HotZeroHeap,
212        true,
213        E::Exact,
214        MB,
215        0,
216        None,
217    ),
218    entry(
219        "https://webizen.org/immersive/function/0.1#nearest",
220        [K::GeometryRef, K::Scalar, K::Boolean, K::Boolean],
221        2,
222        K::AssetRef,
223        X::ColdBoundedSync,
224        true,
225        E::DeterministicApproximate,
226        4 * MB,
227        MB,
228        None,
229    ),
230    // ── Visibility (ray/BVH; exact or labelled approximation) ─────────────────
231    entry(
232        "https://webizen.org/immersive/function/0.1#lineOfSight",
233        [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
234        2,
235        K::Boolean,
236        X::ColdBoundedSync,
237        true,
238        E::Exact,
239        4 * MB,
240        0,
241        None,
242    ),
243    entry(
244        "https://webizen.org/immersive/function/0.1#occludes",
245        [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
246        2,
247        K::Boolean,
248        X::ColdBoundedSync,
249        true,
250        E::DeterministicApproximate,
251        4 * MB,
252        0,
253        None,
254    ),
255    // ── Measurement (hot if precomputed/simple; otherwise bounded) ────────────
256    entry(
257        "https://webizen.org/immersive/function/0.1#volume",
258        [K::GeometryRef, K::Boolean, K::Boolean, K::Boolean],
259        1,
260        K::Quantity,
261        X::ColdBoundedSync,
262        true,
263        E::Exact,
264        4 * MB,
265        0,
266        None,
267    ),
268    entry(
269        "https://webizen.org/immersive/function/0.1#surfaceArea",
270        [K::GeometryRef, K::Boolean, K::Boolean, K::Boolean],
271        1,
272        K::Quantity,
273        X::ColdBoundedSync,
274        true,
275        E::Exact,
276        4 * MB,
277        0,
278        None,
279    ),
280    entry(
281        "https://webizen.org/immersive/function/0.1#centroid",
282        [K::GeometryRef, K::Boolean, K::Boolean, K::Boolean],
283        1,
284        K::GeometryRef,
285        X::HotZeroHeap,
286        true,
287        E::Exact,
288        MB,
289        256,
290        None,
291    ),
292    // ── Temporal (bounded trace/window evaluation) ────────────────────────────
293    entry(
294        "https://webizen.org/immersive/function/0.1#intersectsAt",
295        [K::GeometryRef, K::GeometryRef, K::Instant, K::Boolean],
296        3,
297        K::Boolean,
298        X::ColdBoundedSync,
299        true,
300        E::Exact,
301        4 * MB,
302        0,
303        None,
304    ),
305    entry(
306        "https://webizen.org/immersive/function/0.1#trajectoryIntersects",
307        [K::GeometryRef, K::GeometryRef, K::Interval, K::Boolean],
308        3,
309        K::Boolean,
310        X::ColdBoundedSync,
311        true,
312        E::DeterministicApproximate,
313        4 * MB,
314        0,
315        None,
316    ),
317    entry(
318        "https://webizen.org/immersive/function/0.1#sliceAtTime",
319        [K::TensorRef, K::Instant, K::Boolean, K::Boolean],
320        2,
321        K::TensorRef,
322        X::ColdBoundedSync,
323        true,
324        E::Exact,
325        4 * MB,
326        MB,
327        None,
328    ),
329    // ── Tensor (resident substrate / GPU batch) ───────────────────────────────
330    entry(
331        "https://webizen.org/immersive/function/0.1#tensorDistance",
332        [K::TensorRef, K::TensorRef, K::Boolean, K::Boolean],
333        2,
334        K::Quantity,
335        X::HotZeroHeap,
336        true,
337        E::Exact,
338        256,
339        0,
340        None,
341    ),
342    entry(
343        "https://webizen.org/immersive/function/0.1#tensorWithin",
344        [K::TensorRef, K::TensorRef, K::Scalar, K::Boolean],
345        3,
346        K::Boolean,
347        X::HotZeroHeap,
348        true,
349        E::Exact,
350        256,
351        0,
352        None,
353    ),
354    entry(
355        "https://webizen.org/immersive/function/0.1#tensorSlice",
356        [K::TensorRef, K::Scalar, K::Boolean, K::Boolean],
357        2,
358        K::TensorRef,
359        X::ColdBoundedSync,
360        true,
361        E::Exact,
362        4 * MB,
363        MB,
364        None,
365    ),
366    // knn is a table-producing *graph operator*, not a scalar expression function;
367    // it is registered for discovery/admission but is NOT legal inline (see
368    // `is_expression_legal`). ColdBoundedSync + non-deterministic ordering-of-ties
369    // marks it ineligible for FILTER/BIND (plan §6.1 physical operators).
370    entry(
371        "https://webizen.org/immersive/function/0.1#knn",
372        [K::TensorRef, K::Scalar, K::Boolean, K::Boolean],
373        2,
374        K::AssetRef,
375        X::ColdBoundedSync,
376        false,
377        E::DeterministicApproximate,
378        4 * MB,
379        4 * MB,
380        None,
381    ),
382    // ── Constructive (cold bounded arena) ─────────────────────────────────────
383    entry(
384        "https://webizen.org/immersive/function/0.1#intersectionGeometry",
385        [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
386        2,
387        K::GeometryRef,
388        X::ColdBoundedSync,
389        true,
390        E::Exact,
391        8 * MB,
392        8 * MB,
393        None,
394    ),
395    entry(
396        "https://webizen.org/immersive/function/0.1#unionGeometry",
397        [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
398        2,
399        K::GeometryRef,
400        X::ColdBoundedSync,
401        true,
402        E::Exact,
403        8 * MB,
404        8 * MB,
405        None,
406    ),
407    entry(
408        "https://webizen.org/immersive/function/0.1#differenceGeometry",
409        [K::GeometryRef, K::GeometryRef, K::Boolean, K::Boolean],
410        2,
411        K::GeometryRef,
412        X::ColdBoundedSync,
413        true,
414        E::Exact,
415        8 * MB,
416        8 * MB,
417        None,
418    ),
419    // ── Transform (cold bounded unless a fixed scalar transform) ──────────────
420    entry(
421        "https://webizen.org/immersive/function/0.1#transform",
422        [K::GeometryRef, K::Scalar, K::Boolean, K::Boolean],
423        2,
424        K::GeometryRef,
425        X::ColdBoundedSync,
426        true,
427        E::Exact,
428        8 * MB,
429        8 * MB,
430        None,
431    ),
432    entry(
433        "https://webizen.org/immersive/function/0.1#reproject",
434        [K::GeometryRef, K::Scalar, K::Boolean, K::Boolean],
435        2,
436        K::GeometryRef,
437        X::ColdBoundedSync,
438        true,
439        E::Exact,
440        8 * MB,
441        8 * MB,
442        None,
443    ),
444    entry(
445        "https://webizen.org/immersive/function/0.1#buffer",
446        [K::GeometryRef, K::Scalar, K::Boolean, K::Boolean],
447        2,
448        K::GeometryRef,
449        X::ColdBoundedSync,
450        true,
451        E::DeterministicApproximate,
452        8 * MB,
453        8 * MB,
454        None,
455    ),
456];
457
458/// Look a registered function up by the `q_hash` of its IRI (the value carried in
459/// `Function::Custom(iri_hash)`). Returns `None` for an unregistered function — the
460/// caller then returns a normal SPARQL "unknown function" expression error (never a
461/// fabricated result, plan §7.4).
462pub fn entry_for_iri_hash(iri_hash: u64) -> Option<&'static FunctionEntry> {
463    FUNCTIONS.iter().find(|e| e.descriptor.iri_hash == iri_hash)
464}
465
466/// Look a registered function up by its absolute IRI.
467pub fn entry_for_iri(iri: &str) -> Option<&'static FunctionEntry> {
468    FUNCTIONS.iter().find(|e| e.iri == iri)
469}
470
471/// Admission check for an **inline expression** (`FILTER`/`BIND`) use of a QISP
472/// function identified by `iri_hash`:
473///
474/// - `Ok(entry)` if it is registered and legal inline (deterministic, synchronous,
475///   scalar-producing);
476/// - `Err(NonDeterministicDisallowed)` if registered but async/table-producing
477///   (e.g. `knn`) — it must be invoked as a job / graph operator instead;
478/// - `Ok`-with-`None`-semantics is not used: an unregistered IRI returns `None` so
479///   the evaluator can emit a standard unknown-function error.
480///
481/// This lets the evaluator reject an ill-typed inline call *before* dispatch.
482pub fn admit_inline(iri_hash: u64) -> Option<Result<&'static FunctionEntry, QispError>> {
483    let e = entry_for_iri_hash(iri_hash)?;
484    Some(if e.descriptor.legal_in_filter() {
485        Ok(e)
486    } else {
487        Err(QispError::NonDeterministicDisallowed)
488    })
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use crate::tensor::Tensor10D;
495
496    #[test]
497    fn every_entry_hash_matches_its_iri() {
498        // The const `entry` builder computes iri_hash from the same literal it stores,
499        // so this proves the table is internally consistent (no typo drift).
500        for e in FUNCTIONS {
501            assert_eq!(
502                e.descriptor.iri_hash,
503                crate::q_hash(e.iri),
504                "hash mismatch for {}",
505                e.iri
506            );
507            assert!(
508                e.iri.starts_with(super::super::QISPF_NS),
509                "{} not in qispf: ns",
510                e.iri
511            );
512        }
513    }
514
515    #[test]
516    fn iri_hashes_are_unique() {
517        for (i, a) in FUNCTIONS.iter().enumerate() {
518            for b in &FUNCTIONS[i + 1..] {
519                assert_ne!(
520                    a.descriptor.iri_hash, b.descriptor.iri_hash,
521                    "collision: {} vs {}",
522                    a.iri, b.iri
523                );
524            }
525        }
526    }
527
528    #[test]
529    fn lookup_by_hash_and_iri_agree() {
530        let iri = "https://webizen.org/immersive/function/0.1#intersects";
531        let by_iri = entry_for_iri(iri).unwrap();
532        let by_hash = entry_for_iri_hash(crate::q_hash(iri)).unwrap();
533        assert_eq!(by_iri.descriptor.iri_hash, by_hash.descriptor.iri_hash);
534        assert_eq!(by_iri.descriptor.result, K::Boolean);
535        assert_eq!(by_iri.descriptor.arg_count, 2);
536    }
537
538    #[test]
539    fn topological_and_proximity_defer_to_geosparql() {
540        for name in ["intersects", "contains", "touches", "distance"] {
541            let iri = format!("https://webizen.org/immersive/function/0.1#{name}");
542            let e = entry_for_iri(&iri).unwrap();
543            assert!(e.defers_to.is_some(), "{name} should defer to geof: for 2D");
544        }
545        // withinDistance / volume are QISP-owned (no GeoSPARQL 2D equivalent used here).
546        let wd =
547            entry_for_iri("https://webizen.org/immersive/function/0.1#withinDistance").unwrap();
548        assert!(wd.defers_to.is_none());
549    }
550
551    #[test]
552    fn admission_rejects_knn_inline_but_admits_predicates() {
553        let intersects = crate::q_hash("https://webizen.org/immersive/function/0.1#intersects");
554        assert!(
555            matches!(admit_inline(intersects), Some(Ok(_))),
556            "intersects is legal inline"
557        );
558
559        let knn = crate::q_hash("https://webizen.org/immersive/function/0.1#knn");
560        assert!(
561            matches!(
562                admit_inline(knn),
563                Some(Err(QispError::NonDeterministicDisallowed))
564            ),
565            "knn is a graph operator, not an inline expression function"
566        );
567
568        // Unregistered IRI → None (evaluator emits a standard unknown-function error).
569        assert!(admit_inline(0xDEAD_BEEF_0000_0001).is_none());
570    }
571
572    #[test]
573    fn hot_predicates_are_filter_legal_and_zero_heap() {
574        for name in [
575            "intersects",
576            "contains",
577            "touches",
578            "distance",
579            "withinDistance",
580            "tensorDistance",
581            "tensorWithin",
582        ] {
583            let iri = format!("https://webizen.org/immersive/function/0.1#{name}");
584            let e = entry_for_iri(&iri).unwrap();
585            assert_eq!(e.descriptor.execution, X::HotZeroHeap, "{name} must be hot");
586            assert!(
587                e.descriptor.legal_in_filter(),
588                "{name} must be FILTER-legal"
589            );
590        }
591    }
592
593    #[test]
594    fn constructive_ops_are_cold_bounded_with_output_budget() {
595        for name in [
596            "intersectionGeometry",
597            "unionGeometry",
598            "differenceGeometry",
599        ] {
600            let iri = format!("https://webizen.org/immersive/function/0.1#{name}");
601            let e = entry_for_iri(&iri).unwrap();
602            assert_eq!(e.descriptor.execution, X::ColdBoundedSync);
603            assert!(
604                e.descriptor.max_output_bytes > 0,
605                "{name} must declare an output budget"
606            );
607            assert_eq!(e.descriptor.result, K::GeometryRef);
608        }
609    }
610
611    #[test]
612    fn tensor_predicates_use_canonical_metric_and_reject_bad_radius() {
613        let a = Tensor10D::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
614        let b = Tensor10D::new(0.0, 0.0, 0.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0, 0.0);
615        assert_eq!(tensor_distance(&a, &b).unwrap(), a.full_distance(&b));
616        assert_eq!(tensor_within(&a, &b, 5.0), Ok(true));
617        assert_eq!(tensor_within(&a, &b, 4.99), Ok(false));
618        assert_eq!(
619            tensor_within(&a, &b, f32::NAN),
620            Err(QispError::ProfileMismatch)
621        );
622        assert_eq!(tensor_within(&a, &b, -1.0), Err(QispError::ProfileMismatch));
623    }
624
625    #[test]
626    fn tensor_knn_is_bounded_sorted_and_tie_deterministic() {
627        let query = Tensor10D::default();
628        let candidates = [
629            Tensor10D::new(0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
630            Tensor10D::new(0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
631            Tensor10D::new(0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
632        ];
633        let mut out = [TensorNeighbor {
634            index: 99,
635            distance: f32::INFINITY,
636        }; 2];
637        assert_eq!(tensor_knn_into(&query, &candidates, 2, &mut out), Ok(2));
638        assert_eq!(out[0].index, 1);
639        assert_eq!(out[1].index, 2);
640        assert_eq!(out[0].distance, out[1].distance);
641        assert_eq!(
642            tensor_knn_into(&query, &candidates, 3, &mut out),
643            Err(QispError::BudgetExceeded)
644        );
645    }
646}