Skip to main content

qualia_core_db/sparql_library/immersive/
value.rs

1//! QISP typed value model — the typed function descriptor, execution/exactness
2//! classification, and the stable QISP error codes.
3//!
4//! This module is the *typed signature* foundation for the Immersive SPARQL (QISP)
5//! profile (plan §4.2 "No untyped `u64 -> u64` public function contract", §4.3
6//! "Error semantics", §4.4 "exactness profiles"). Every type here is a fixed-size,
7//! `#[repr(C)]`, `Copy` record with **no `String`/`Box`/`Vec` inside** — it belongs
8//! on the zero-heap evaluation tier, matching the house idiom in `sparql_ast.rs`.
9//!
10//! Provisional, Editor's-Draft IRIs (no compatibility promise) — see the namespace
11//! constants in the parent [`mod`](super).
12
13/// The closed set of typed value kinds a QISP function may consume or produce.
14///
15/// Reproduced verbatim from plan §4.2. `AssetRef`/`GeometryRef`/`TensorRef` are
16/// *validated, process-local* handles resolved from an absolute RDF IRI — never an
17/// opaque numeric pointer in the public term (plan §3.6, §2.2 item 5).
18#[repr(u8)]
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum ImmersiveValueKind {
21    /// `xsd:boolean` — predicate result.
22    Boolean = 0,
23    /// A dimensionless numeric literal (`xsd:double`/`xsd:decimal`/…).
24    Scalar = 1,
25    /// A numeric measurement that MUST carry a unit (QUDT-described).
26    Quantity = 2,
27    /// A validated reference to a content-addressed dense asset.
28    AssetRef = 3,
29    /// A validated reference to a (possibly query-scoped) geometry.
30    GeometryRef = 4,
31    /// A validated reference to a Tensor10D buffer/section.
32    TensorRef = 5,
33    /// An `xsd:dateTime`/OWL-Time instant.
34    Instant = 6,
35    /// An OWL-Time interval.
36    Interval = 7,
37}
38
39/// How a function is allowed to execute (plan §4.2 / §6.1).
40///
41/// `HotZeroHeap` and `ColdBoundedSync` may run inside a synchronous SPARQL
42/// expression (FILTER/BIND); `AsyncRequired` must go through the job API and is
43/// therefore illegal in an inline expression.
44#[repr(u8)]
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub enum ExecutionClass {
47    /// Zero allocation, runs in the hot predicate path.
48    HotZeroHeap = 0,
49    /// Bounded, synchronous; may use a caller-owned/byte-budgeted cold workspace.
50    ColdBoundedSync = 1,
51    /// Cannot run synchronously; must be submitted as a QISP job.
52    AsyncRequired = 2,
53}
54
55/// Exactness profile (plan §4.4). Exactness is an explicit contract, never an
56/// invisible server preference.
57#[repr(u8)]
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub enum ExactnessClass {
60    /// `qisp:Exact` — robust/exact predicates and constructions where supported.
61    Exact = 0,
62    /// `qisp:DeterministicApproximate` — reproducible approximation with declared
63    /// absolute/relative error bounds.
64    DeterministicApproximate = 1,
65    /// `qisp:InteractiveApproximate` — renderer/GPU-oriented result; never accepted
66    /// for a rights-affecting policy without a separate exact verification.
67    InteractiveApproximate = 2,
68}
69
70impl ExactnessClass {
71    /// The provisional `qisp:` IRI for this exactness profile.
72    ///
73    /// The returned literal is `QISP_NS` + the profile local name; a unit test
74    /// asserts that invariant so the two stay in lockstep.
75    pub const fn iri(&self) -> &'static str {
76        match self {
77            ExactnessClass::Exact => "https://webizen.org/immersive/0.1#Exact",
78            ExactnessClass::DeterministicApproximate => {
79                "https://webizen.org/immersive/0.1#DeterministicApproximate"
80            }
81            ExactnessClass::InteractiveApproximate => {
82                "https://webizen.org/immersive/0.1#InteractiveApproximate"
83            }
84        }
85    }
86}
87
88/// Typed function descriptor for a QISP extension function (plan §4.2, verbatim
89/// field set plus the `ExactnessClass` referenced there).
90///
91/// Fixed-size, `#[repr(C)]`, `Copy` — connects a function IRI (as a compile-time
92/// `q_hash`) to its typed signature, execution class, determinism, exactness, and
93/// I/O byte budgets. It carries no owned buffers; the resource limits reference the
94/// canonical geometry capability manifests rather than duplicating them.
95#[repr(C)]
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct ImmersiveFunctionDescriptor {
98    /// `q_hash` of the function's absolute IRI (e.g. `qispf:intersects`).
99    pub iri_hash: u64,
100    /// Positional argument kinds (only the first `arg_count` are meaningful).
101    pub args: [ImmersiveValueKind; 4],
102    /// Number of populated entries in `args` (0..=4).
103    pub arg_count: u8,
104    /// Result value kind.
105    pub result: ImmersiveValueKind,
106    /// Execution class (hot / cold-sync / async).
107    pub execution: ExecutionClass,
108    /// Whether repeated evaluation with the same key yields the same term
109    /// (referential transparency within a query snapshot — plan §4.4).
110    pub deterministic: bool,
111    /// Exactness profile the descriptor is registered under.
112    pub exactness: ExactnessClass,
113    /// Maximum accepted input size in bytes (admission control).
114    pub max_input_bytes: u32,
115    /// Maximum producible output size in bytes (admission control).
116    pub max_output_bytes: u32,
117}
118
119impl ImmersiveFunctionDescriptor {
120    /// Const-friendly constructor so registries can be built in `const`/`static`
121    /// tables. `arg_count` is clamped to the 4-slot capacity.
122    #[allow(clippy::too_many_arguments)]
123    pub const fn new(
124        iri_hash: u64,
125        args: [ImmersiveValueKind; 4],
126        arg_count: u8,
127        result: ImmersiveValueKind,
128        execution: ExecutionClass,
129        deterministic: bool,
130        exactness: ExactnessClass,
131        max_input_bytes: u32,
132        max_output_bytes: u32,
133    ) -> Self {
134        let arg_count = if arg_count > 4 { 4 } else { arg_count };
135        Self {
136            iri_hash,
137            args,
138            arg_count,
139            result,
140            execution,
141            deterministic,
142            exactness,
143            max_input_bytes,
144            max_output_bytes,
145        }
146    }
147
148    /// The meaningful (populated) argument kinds.
149    pub fn arg_kinds(&self) -> &[ImmersiveValueKind] {
150        &self.args[..(self.arg_count as usize)]
151    }
152
153    /// Whether this function must be submitted as a job (cannot run inline).
154    pub const fn is_async(&self) -> bool {
155        matches!(self.execution, ExecutionClass::AsyncRequired)
156    }
157
158    /// Whether this function is legal inside a SPARQL `FILTER`.
159    ///
160    /// A `HotZeroHeap` or `ColdBoundedSync` **deterministic** function is legal;
161    /// an `AsyncRequired` or non-deterministic function is not (plan §4.2, §4.4).
162    pub const fn legal_in_filter(&self) -> bool {
163        self.deterministic && !self.is_async()
164    }
165
166    /// Whether this function is legal inside a SPARQL `BIND`. Same rule as
167    /// `FILTER`: snapshot-pure, synchronous, deterministic (plan §4.4).
168    pub const fn legal_in_bind(&self) -> bool {
169        self.legal_in_filter()
170    }
171}
172
173/// Stable QISP error codes (plan §4.3). SPARQL expression errors stay expression
174/// errors — they MUST NOT silently become `false`. Each variant has a stable
175/// kebab-case `code()` for machine-readable diagnostics and Problem-Details type
176/// IRIs.
177#[repr(u8)]
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179pub enum QispError {
180    /// The asset reference does not resolve to a known/valid record.
181    UnknownAsset = 0,
182    /// The asset reference is syntactically known but its generation is stale.
183    StaleAsset = 1,
184    /// Unsupported CRS or profile conversion.
185    UnsupportedCrsOrProfile = 2,
186    /// Invalid or non-manifold geometry.
187    InvalidGeometry = 3,
188    /// Dimension/profile mismatch (e.g. wrong Tensor10D arity/profile).
189    ProfileMismatch = 4,
190    /// Output or workspace byte/work budget exceeded.
191    BudgetExceeded = 5,
192    /// The requested exactness profile is unavailable for this operation.
193    ExactnessUnavailable = 6,
194    /// Authorization denied.
195    AuthorizationDenied = 7,
196    /// The computation was cancelled or its lease expired.
197    CancelledOrExpired = 8,
198    /// A non-deterministic backend was requested where it is disallowed.
199    NonDeterministicDisallowed = 9,
200}
201
202impl QispError {
203    /// Stable, machine-readable kebab-case error code. These strings are part of
204    /// the profile's external contract and must not be renamed without a namespace
205    /// version bump (plan §2.4 "Terms are never silently repurposed").
206    pub const fn code(&self) -> &'static str {
207        match self {
208            QispError::UnknownAsset => "unknown-asset",
209            QispError::StaleAsset => "stale-asset",
210            QispError::UnsupportedCrsOrProfile => "unsupported-crs-or-profile",
211            QispError::InvalidGeometry => "invalid-geometry",
212            QispError::ProfileMismatch => "profile-mismatch",
213            QispError::BudgetExceeded => "budget-exceeded",
214            QispError::ExactnessUnavailable => "exactness-unavailable",
215            QispError::AuthorizationDenied => "authorization-denied",
216            QispError::CancelledOrExpired => "cancelled-or-expired",
217            QispError::NonDeterministicDisallowed => "non-deterministic-disallowed",
218        }
219    }
220
221    /// A short human-readable description (never leaks internal paths/addresses,
222    /// per plan §7.2 / §10.1 QISP-R04).
223    pub const fn message(&self) -> &'static str {
224        match self {
225            QispError::UnknownAsset => "unknown or invalid dense asset reference",
226            QispError::StaleAsset => "stale dense asset reference (generation mismatch)",
227            QispError::UnsupportedCrsOrProfile => "unsupported CRS or profile conversion",
228            QispError::InvalidGeometry => "invalid or non-manifold geometry",
229            QispError::ProfileMismatch => "dimension or profile mismatch",
230            QispError::BudgetExceeded => "output or workspace budget exceeded",
231            QispError::ExactnessUnavailable => "requested exactness profile is unavailable",
232            QispError::AuthorizationDenied => "authorization denied",
233            QispError::CancelledOrExpired => "computation cancelled or expired",
234            QispError::NonDeterministicDisallowed => "non-deterministic backend disallowed here",
235        }
236    }
237}
238
239impl core::fmt::Display for QispError {
240    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
241        write!(f, "qisp[{}]: {}", self.code(), self.message())
242    }
243}
244
245impl std::error::Error for QispError {}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use std::mem::size_of;
251
252    #[test]
253    fn descriptor_is_a_small_fixed_repr_c_record() {
254        // Fixed, deterministic `#[repr(C)]` layout: u64 + [u8;4] + u8*4 + pad + u32*2.
255        // If this changes, the ABI changed — update deliberately, don't paper over it.
256        assert_eq!(size_of::<ImmersiveFunctionDescriptor>(), 32);
257        assert_eq!(size_of::<ImmersiveValueKind>(), 1);
258        assert_eq!(size_of::<ExecutionClass>(), 1);
259        assert_eq!(size_of::<ExactnessClass>(), 1);
260        // No owned buffers: the descriptor is Copy.
261        fn assert_copy<T: Copy>() {}
262        assert_copy::<ImmersiveFunctionDescriptor>();
263    }
264
265    #[test]
266    fn descriptor_round_trips_by_value() {
267        let d = ImmersiveFunctionDescriptor::new(
268            crate::q_hash("https://webizen.org/immersive/function/0.1#intersects"),
269            [
270                ImmersiveValueKind::AssetRef,
271                ImmersiveValueKind::AssetRef,
272                ImmersiveValueKind::Boolean,
273                ImmersiveValueKind::Boolean,
274            ],
275            2,
276            ImmersiveValueKind::Boolean,
277            ExecutionClass::HotZeroHeap,
278            true,
279            ExactnessClass::Exact,
280            1 << 20,
281            0,
282        );
283        let copy = d; // Copy
284        assert_eq!(d, copy);
285        assert_eq!(d.arg_count, 2);
286        assert_eq!(
287            d.arg_kinds(),
288            &[ImmersiveValueKind::AssetRef, ImmersiveValueKind::AssetRef]
289        );
290        assert_eq!(d.result, ImmersiveValueKind::Boolean);
291    }
292
293    #[test]
294    fn arg_count_is_clamped() {
295        let d = ImmersiveFunctionDescriptor::new(
296            0,
297            [ImmersiveValueKind::Scalar; 4],
298            9,
299            ImmersiveValueKind::Scalar,
300            ExecutionClass::HotZeroHeap,
301            true,
302            ExactnessClass::Exact,
303            0,
304            0,
305        );
306        assert_eq!(d.arg_count, 4);
307        assert_eq!(d.arg_kinds().len(), 4);
308    }
309
310    #[test]
311    fn filter_bind_legality() {
312        let hot = ImmersiveFunctionDescriptor::new(
313            0,
314            [ImmersiveValueKind::AssetRef; 4],
315            1,
316            ImmersiveValueKind::Boolean,
317            ExecutionClass::HotZeroHeap,
318            true,
319            ExactnessClass::Exact,
320            0,
321            0,
322        );
323        assert!(hot.legal_in_filter());
324        assert!(hot.legal_in_bind());
325
326        let cold = ImmersiveFunctionDescriptor {
327            execution: ExecutionClass::ColdBoundedSync,
328            ..hot
329        };
330        assert!(cold.legal_in_filter());
331
332        // AsyncRequired is never legal inline.
333        let async_fn = ImmersiveFunctionDescriptor {
334            execution: ExecutionClass::AsyncRequired,
335            ..hot
336        };
337        assert!(!async_fn.legal_in_filter());
338        assert!(!async_fn.legal_in_bind());
339        assert!(async_fn.is_async());
340
341        // Non-deterministic is never legal inline (optimizer may reorder/repeat).
342        let nondet = ImmersiveFunctionDescriptor {
343            deterministic: false,
344            ..hot
345        };
346        assert!(!nondet.legal_in_filter());
347    }
348
349    #[test]
350    fn exactness_iris_are_stable_and_namespaced() {
351        use super::super::QISP_NS;
352        assert!(ExactnessClass::Exact.iri().starts_with(QISP_NS));
353        assert!(ExactnessClass::DeterministicApproximate
354            .iri()
355            .starts_with(QISP_NS));
356        assert!(ExactnessClass::InteractiveApproximate
357            .iri()
358            .starts_with(QISP_NS));
359        assert_eq!(
360            ExactnessClass::Exact.iri(),
361            "https://webizen.org/immersive/0.1#Exact"
362        );
363        assert_eq!(
364            ExactnessClass::DeterministicApproximate.iri(),
365            "https://webizen.org/immersive/0.1#DeterministicApproximate"
366        );
367        assert_eq!(
368            ExactnessClass::InteractiveApproximate.iri(),
369            "https://webizen.org/immersive/0.1#InteractiveApproximate"
370        );
371    }
372
373    #[test]
374    fn error_codes_are_stable() {
375        assert_eq!(QispError::UnknownAsset.code(), "unknown-asset");
376        assert_eq!(QispError::StaleAsset.code(), "stale-asset");
377        assert_eq!(
378            QispError::UnsupportedCrsOrProfile.code(),
379            "unsupported-crs-or-profile"
380        );
381        assert_eq!(QispError::InvalidGeometry.code(), "invalid-geometry");
382        assert_eq!(QispError::ProfileMismatch.code(), "profile-mismatch");
383        assert_eq!(QispError::BudgetExceeded.code(), "budget-exceeded");
384        assert_eq!(
385            QispError::ExactnessUnavailable.code(),
386            "exactness-unavailable"
387        );
388        assert_eq!(
389            QispError::AuthorizationDenied.code(),
390            "authorization-denied"
391        );
392        assert_eq!(QispError::CancelledOrExpired.code(), "cancelled-or-expired");
393        assert_eq!(
394            QispError::NonDeterministicDisallowed.code(),
395            "non-deterministic-disallowed"
396        );
397    }
398
399    #[test]
400    fn error_display_and_std_error() {
401        let e = QispError::StaleAsset;
402        let s = format!("{e}");
403        assert!(s.contains("stale-asset"));
404        // Usable as a std::error::Error trait object.
405        let _boxed: Box<dyn std::error::Error> = Box::new(e);
406    }
407}