Skip to main content

xlog_gpu/
logic.rs

1//! GPU-accelerated evaluation of compiled Datalog programs.
2
3use std::cmp::Ordering as CmpOrdering;
4use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
5use std::path::{Path, PathBuf};
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Arc, Mutex, OnceLock};
8
9use xlog_core::{resolve_bool, symbol, RelId, Result, ScalarType, Schema, XlogError};
10use xlog_cuda::{CudaBuffer, CudaColumn, CudaKernelProvider};
11use xlog_ir::{EpistemicExecutablePlan, ExecutionPlan};
12use xlog_logic::ast::{PredColumn, PredDecl, TypeRef};
13use xlog_logic::epistemic::{
14    compile_epistemic_gpu_execution, compile_epistemic_gpu_split_execution,
15    epistemic_extensional_multi_arity_predicates, prepare_epistemic_program,
16    reduce_epistemic_program_to_ordinary,
17    reduce_epistemic_program_to_ordinary_for_stratified_schema,
18    try_plan_stratified_epistemic_program, try_prepare_g91_compatibility_reduction,
19    try_reduce_case_a_recursive_epistemic_program, try_reduce_prepared_recursive_epistemic_program,
20    EpistemicSplitExecutablePlan, G91CompatibilityReduction,
21};
22use xlog_logic::ground_term_encoding::append_ground_term_bytes;
23use xlog_logic::{
24    format_constraint_body, Atom, BodyLiteral, Compiler, Constraint, EpistemicLiteral, EpistemicOp,
25    Program, Query, Rule, Term,
26};
27use xlog_runtime::executor::{JoinIndexCacheStats, PreparedResidentGraph};
28use xlog_runtime::resident_graph::{
29    ResidentGraphCertifiedPlan, ResidentGraphCoreTransferStats, ResidentGraphDeclineReason,
30    ResidentGraphDeferredProfile, ResidentGraphExecutionError, ResidentGraphExecutionStats,
31    ResidentGraphFinalObservationStats, ResidentGraphPrepareOptions, ResidentGraphSchemaCatalog,
32    ResidentGraphSelectionKind,
33};
34use xlog_runtime::{
35    DeltaRecomputeStats, EpistemicGpuExecutionResult, EpistemicGpuWorkspaceCapacities,
36    ExecutionStats, Executor, OpStats, RelationDelta, RelationStore, StratumStats,
37};
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40enum ResidentSelectionMode {
41    Auto,
42    Disabled,
43    Require,
44}
45
46impl ResidentSelectionMode {
47    fn from_env() -> Result<Self> {
48        let disabled = resolve_bool(None, "XLOG_DISABLE_RESIDENT_RECURSION", false)?;
49        let required = resolve_bool(None, "XLOG_REQUIRE_RESIDENT_RECURSION", false)?;
50        if disabled && required {
51            return Err(XlogError::Execution(
52                "resident execution environment flags are mutually exclusive".to_string(),
53            ));
54        }
55        Ok(if disabled {
56            Self::Disabled
57        } else if required {
58            Self::Require
59        } else {
60            Self::Auto
61        })
62    }
63
64    fn enabled(self) -> bool {
65        self != Self::Disabled
66    }
67}
68
69struct ResidentCompletedProfile {
70    telemetry: ResidentGraphExecutionStats,
71    iterations: u32,
72}
73
74const RESIDENT_LATENCY_DIAGNOSTICS_ENV: &str = "XLOG_RESIDENT_LATENCY_DIAGNOSTICS";
75static RESIDENT_LATENCY_SAMPLE: AtomicU64 = AtomicU64::new(0);
76
77#[derive(Default)]
78struct ResidentLatencyDiagnostic {
79    sample: u64,
80    certificate_input_ns: u64,
81    certificate_cache_was_warm: bool,
82    certificate_initialized_here: bool,
83    certificate_initialization_ns: u64,
84    certificate_cache_access_ns: u64,
85    input_setup_ns: u64,
86    prepare_capture_allocation_ns: u64,
87    launch_submission_ns: u64,
88    sync_wall_ns: u64,
89    device_event_ns: u64,
90    receipt_d2h_ns: u64,
91    receipt_decode_schema_staging_ns: u64,
92    owner_teardown_residual_ns: u64,
93    commit_ns: u64,
94    result_stats_construction_ns: u64,
95    executor_store_teardown_ns: u64,
96    staged_outputs: u64,
97    relation_registrations: usize,
98    remaining_store_relations_before_drop: usize,
99    runtime_bytes: [usize; 8],
100    manager_bytes: [u64; 8],
101}
102
103#[cfg(test)]
104mod resident_latency_diagnostic_tests {
105    use super::{
106        finalized_resident_latency_diagnostic_lines, resident_latency_diagnostic_line,
107        ResidentLatencyDiagnostic,
108    };
109
110    #[test]
111    fn certificate_latency_distinguishes_cold_initialization_from_warm_access() {
112        let mut cold = ResidentLatencyDiagnostic::new();
113        cold.sample = 3;
114        cold.certificate_cache_was_warm = false;
115        cold.certificate_initialized_here = true;
116        cold.certificate_initialization_ns = 41;
117        let cold_line =
118            resident_latency_diagnostic_line(Some(&cold), 101).expect("cold diagnostic line");
119        assert!(cold_line.contains("sample=3"));
120        assert!(cold_line.contains("certificate_cache_was_warm=false"));
121        assert!(cold_line.contains("certificate_initialized_here=true"));
122        assert!(cold_line.contains("certificate_initialization_ns=41"));
123        assert!(cold_line.contains("certificate_cache_access_ns=0"));
124
125        let mut warm = ResidentLatencyDiagnostic::new();
126        warm.sample = 4;
127        warm.certificate_cache_was_warm = true;
128        warm.certificate_cache_access_ns = 7;
129        let warm_line =
130            resident_latency_diagnostic_line(Some(&warm), 102).expect("warm diagnostic line");
131        assert!(warm_line.contains("sample=4"));
132        assert!(warm_line.contains("certificate_cache_was_warm=true"));
133        assert!(warm_line.contains("certificate_initialized_here=false"));
134        assert!(warm_line.contains("certificate_initialization_ns=0"));
135        assert!(warm_line.contains("certificate_cache_access_ns=7"));
136        assert_eq!(resident_latency_diagnostic_line(None, 0), None);
137    }
138
139    #[test]
140    fn finalized_latency_diagnostics_derive_after_total_and_preserve_sample_order() {
141        let mut outer = ResidentLatencyDiagnostic::new();
142        outer.sample = 17;
143        let prepare_derived = std::cell::Cell::new(false);
144        let outer_derived = std::cell::Cell::new(false);
145
146        let lines = finalized_resident_latency_diagnostic_lines(
147            101,
148            Some(&outer),
149            Some(|| {
150                prepare_derived.set(true);
151                "resident prepare phases: sample=17 total_ns=53".to_string()
152            }),
153            |diagnostic, total_ns| {
154                outer_derived.set(true);
155                diagnostic.format_line(total_ns)
156            },
157        )
158        .expect("enabled diagnostics finalize one fixed pair");
159
160        assert!(prepare_derived.get());
161        assert!(outer_derived.get());
162        assert_eq!(lines.len(), 2);
163        assert!(lines[0]
164            .as_deref()
165            .is_some_and(|line| line.starts_with("resident prepare phases: sample=17 ")));
166        assert!(lines[1].as_deref().is_some_and(|line| {
167            line.starts_with("resident latency phases: sample=17 total_ns=101 ")
168        }));
169
170        let disabled_prepare_called = std::cell::Cell::new(false);
171        let disabled_outer_called = std::cell::Cell::new(false);
172        let disabled = finalized_resident_latency_diagnostic_lines(
173            0,
174            None,
175            Some(|| {
176                disabled_prepare_called.set(true);
177                String::new()
178            }),
179            |diagnostic, total_ns| {
180                disabled_outer_called.set(true);
181                diagnostic.format_line(total_ns)
182            },
183        );
184        assert!(disabled.is_none());
185        assert!(!disabled_prepare_called.get());
186        assert!(!disabled_outer_called.get());
187    }
188}
189
190impl ResidentLatencyDiagnostic {
191    fn new() -> Self {
192        Self {
193            sample: RESIDENT_LATENCY_SAMPLE.fetch_add(1, Ordering::Relaxed),
194            ..Self::default()
195        }
196    }
197
198    fn format_line(&self, total_ns: u64) -> String {
199        let additive_phases = [
200            self.certificate_input_ns,
201            self.prepare_capture_allocation_ns,
202            self.launch_submission_ns,
203            self.sync_wall_ns,
204            self.receipt_d2h_ns,
205            self.receipt_decode_schema_staging_ns,
206            self.owner_teardown_residual_ns,
207            self.commit_ns,
208            self.result_stats_construction_ns,
209            self.executor_store_teardown_ns,
210        ];
211        let unattributed_host_ns = resident_latency_unattributed_ns(total_ns, &additive_phases);
212        let owner_runtime_bytes_released =
213            self.runtime_bytes[4].saturating_sub(self.runtime_bytes[5]);
214        let owner_manager_bytes_released =
215            self.manager_bytes[4].saturating_sub(self.manager_bytes[5]);
216        let executor_runtime_bytes_released =
217            self.runtime_bytes[6].saturating_sub(self.runtime_bytes[7]);
218        let executor_manager_bytes_released =
219            self.manager_bytes[6].saturating_sub(self.manager_bytes[7]);
220        format!(
221            "resident latency phases: sample={} total_ns={} certificate_input_ns={} certificate_cache_was_warm={} certificate_initialized_here={} certificate_initialization_ns={} certificate_cache_access_ns={} input_setup_ns={} certificate_input_unattributed_ns={} prepare_capture_allocation_ns={} launch_submission_ns={} sync_wall_ns={} device_event_ns_nonadditive={} receipt_d2h_ns={} receipt_decode_schema_staging_ns={} owner_teardown_residual_ns={} commit_ns={} result_stats_construction_ns={} executor_store_teardown_ns={} unattributed_host_ns={} staged_outputs={} relation_registrations={} remaining_store_relations_before_drop={} allocation_snapshot_order=runtime_ready|after_setup|after_prepare|after_launch|after_sync|after_observe|after_commit|after_executor_drop runtime_bytes={:?} manager_bytes={:?} owner_runtime_bytes_released={} owner_manager_bytes_released={} executor_runtime_bytes_released={} executor_manager_bytes_released={} deallocation_calls=unavailable",
222            self.sample,
223            total_ns,
224            self.certificate_input_ns,
225            self.certificate_cache_was_warm,
226            self.certificate_initialized_here,
227            self.certificate_initialization_ns,
228            self.certificate_cache_access_ns,
229            self.input_setup_ns,
230            self.certificate_input_ns
231                .saturating_sub(self.certificate_initialization_ns)
232                .saturating_sub(self.certificate_cache_access_ns)
233                .saturating_sub(self.input_setup_ns),
234            self.prepare_capture_allocation_ns,
235            self.launch_submission_ns,
236            self.sync_wall_ns,
237            self.device_event_ns,
238            self.receipt_d2h_ns,
239            self.receipt_decode_schema_staging_ns,
240            self.owner_teardown_residual_ns,
241            self.commit_ns,
242            self.result_stats_construction_ns,
243            self.executor_store_teardown_ns,
244            unattributed_host_ns,
245            self.staged_outputs,
246            self.relation_registrations,
247            self.remaining_store_relations_before_drop,
248            self.runtime_bytes,
249            self.manager_bytes,
250            owner_runtime_bytes_released,
251            owner_manager_bytes_released,
252            executor_runtime_bytes_released,
253            executor_manager_bytes_released,
254        )
255    }
256}
257
258#[cfg(test)]
259fn resident_latency_diagnostic_line(
260    diagnostic: Option<&ResidentLatencyDiagnostic>,
261    total_ns: u64,
262) -> Option<String> {
263    diagnostic.map(|diagnostic| diagnostic.format_line(total_ns))
264}
265
266fn finalized_resident_latency_diagnostic_lines<F, G>(
267    total_ns: u64,
268    diagnostic: Option<&ResidentLatencyDiagnostic>,
269    prepare_line: Option<F>,
270    format_outer: G,
271) -> Option<[Option<String>; 2]>
272where
273    F: FnOnce() -> String,
274    G: FnOnce(&ResidentLatencyDiagnostic, u64) -> String,
275{
276    let diagnostic = diagnostic?;
277    Some([
278        prepare_line.map(|prepare_line| prepare_line()),
279        Some(format_outer(diagnostic, total_ns)),
280    ])
281}
282
283fn resident_latency_diagnostics_enabled() -> Result<bool> {
284    resolve_bool(None, RESIDENT_LATENCY_DIAGNOSTICS_ENV, false)
285}
286
287fn resident_latency_elapsed_ns(started: Option<std::time::Instant>) -> u64 {
288    started
289        .map(|started| u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX))
290        .unwrap_or(0)
291}
292
293fn resident_latency_unattributed_ns(total_ns: u64, phases: &[u64]) -> u64 {
294    phases.iter().copied().fold(total_ns, u64::saturating_sub)
295}
296
297/// Result of evaluating a single query in a Datalog program.
298pub struct LogicQueryResult {
299    /// Display relation name. Ordinary query projections use an internal name such as
300    /// `__xlog_query_0`; epistemic materializations use the source predicate name.
301    pub relation_name: String,
302    /// Output variable names in column order.
303    pub columns: Vec<String>,
304    /// Per-output-column sort labels in column order.
305    pub sort_labels: Vec<String>,
306    /// GPU-resident column buffer with the result tuples.
307    pub buffer: CudaBuffer,
308}
309
310/// A typed scalar downloaded from a completed GPU relation.
311///
312/// This representation is intentionally independent of Arrow so runtime
313/// consumers can serialize exact relation rows without losing XLOG's logical
314/// scalar types. Symbol IDs are resolved through the process symbol table and
315/// invalid IDs fail closed.
316#[derive(Clone, Debug, PartialEq)]
317pub enum LogicScalarValue {
318    /// Unsigned 32-bit integer.
319    U32(u32),
320    /// Unsigned 64-bit integer.
321    U64(u64),
322    /// Signed 32-bit integer.
323    I32(i32),
324    /// Signed 64-bit integer.
325    I64(i64),
326    /// 32-bit IEEE 754 floating-point value.
327    F32(f32),
328    /// 64-bit IEEE 754 floating-point value.
329    F64(f64),
330    /// Boolean value.
331    Bool(bool),
332    /// Resolved dictionary-encoded symbol.
333    Symbol(String),
334}
335
336enum DownloadedLogicColumn {
337    U32(Vec<u32>),
338    U64(Vec<u64>),
339    I32(Vec<i32>),
340    I64(Vec<i64>),
341    F32(Vec<f32>),
342    F64(Vec<f64>),
343    Bool(Vec<bool>),
344    Symbol(Vec<String>),
345}
346
347impl DownloadedLogicColumn {
348    fn value(&self, row: usize) -> LogicScalarValue {
349        match self {
350            Self::U32(values) => LogicScalarValue::U32(values[row]),
351            Self::U64(values) => LogicScalarValue::U64(values[row]),
352            Self::I32(values) => LogicScalarValue::I32(values[row]),
353            Self::I64(values) => LogicScalarValue::I64(values[row]),
354            Self::F32(values) => LogicScalarValue::F32(values[row]),
355            Self::F64(values) => LogicScalarValue::F64(values[row]),
356            Self::Bool(values) => LogicScalarValue::Bool(values[row]),
357            Self::Symbol(values) => LogicScalarValue::Symbol(values[row].clone()),
358        }
359    }
360}
361
362/// Download a completed GPU relation into deterministic typed rows.
363///
364/// The GPU remains the source of truth for evaluation. This function performs
365/// an explicit post-run D2H export for external evidence consumers; it is not
366/// part of the resident evaluation hot loop.
367pub fn download_logic_rows(
368    provider: &CudaKernelProvider,
369    buffer: &CudaBuffer,
370) -> Result<Vec<Vec<LogicScalarValue>>> {
371    let row_count = provider.validated_logical_row_count(buffer)?;
372    let schema = buffer.schema();
373    if schema.arity() == 0 {
374        return Ok(vec![Vec::new(); row_count]);
375    }
376
377    let mut columns = Vec::with_capacity(schema.arity());
378    for column_index in 0..schema.arity() {
379        let scalar_type = schema.column_type(column_index).ok_or_else(|| {
380            XlogError::Execution(format!(
381                "relation schema is missing scalar type for column {column_index}"
382            ))
383        })?;
384        let column = match scalar_type {
385            ScalarType::U32 => {
386                DownloadedLogicColumn::U32(provider.download_column(buffer, column_index)?)
387            }
388            ScalarType::U64 => {
389                DownloadedLogicColumn::U64(provider.download_column(buffer, column_index)?)
390            }
391            ScalarType::I32 => {
392                DownloadedLogicColumn::I32(provider.download_column(buffer, column_index)?)
393            }
394            ScalarType::I64 => {
395                DownloadedLogicColumn::I64(provider.download_column(buffer, column_index)?)
396            }
397            ScalarType::F32 => {
398                DownloadedLogicColumn::F32(provider.download_column(buffer, column_index)?)
399            }
400            ScalarType::F64 => {
401                DownloadedLogicColumn::F64(provider.download_column(buffer, column_index)?)
402            }
403            ScalarType::Bool => {
404                DownloadedLogicColumn::Bool(provider.download_column(buffer, column_index)?)
405            }
406            ScalarType::Symbol => {
407                let ids: Vec<u32> = provider.download_column(buffer, column_index)?;
408                let values = ids
409                    .into_iter()
410                    .map(|id| {
411                        symbol::resolve_checked(id).ok_or_else(|| {
412                            XlogError::Execution(format!(
413                                "relation column {column_index} contains unknown symbol ID {id}"
414                            ))
415                        })
416                    })
417                    .collect::<Result<Vec<_>>>()?;
418                DownloadedLogicColumn::Symbol(values)
419            }
420        };
421        columns.push(column);
422    }
423
424    let mut rows = (0..row_count)
425        .map(|row| columns.iter().map(|column| column.value(row)).collect())
426        .collect::<Vec<Vec<LogicScalarValue>>>();
427    rows.sort_by(|left, right| compare_logic_rows(left, right));
428    Ok(rows)
429}
430
431fn compare_logic_rows(left: &[LogicScalarValue], right: &[LogicScalarValue]) -> CmpOrdering {
432    left.iter()
433        .zip(right)
434        .map(|(left, right)| compare_logic_values(left, right))
435        .find(|ordering| *ordering != CmpOrdering::Equal)
436        .unwrap_or_else(|| left.len().cmp(&right.len()))
437}
438
439fn compare_logic_values(left: &LogicScalarValue, right: &LogicScalarValue) -> CmpOrdering {
440    match (left, right) {
441        (LogicScalarValue::U32(left), LogicScalarValue::U32(right)) => left.cmp(right),
442        (LogicScalarValue::U64(left), LogicScalarValue::U64(right)) => left.cmp(right),
443        (LogicScalarValue::I32(left), LogicScalarValue::I32(right)) => left.cmp(right),
444        (LogicScalarValue::I64(left), LogicScalarValue::I64(right)) => left.cmp(right),
445        (LogicScalarValue::F32(left), LogicScalarValue::F32(right)) => left.total_cmp(right),
446        (LogicScalarValue::F64(left), LogicScalarValue::F64(right)) => left.total_cmp(right),
447        (LogicScalarValue::Bool(left), LogicScalarValue::Bool(right)) => left.cmp(right),
448        (LogicScalarValue::Symbol(left), LogicScalarValue::Symbol(right)) => left.cmp(right),
449        _ => logic_value_rank(left).cmp(&logic_value_rank(right)),
450    }
451}
452
453fn logic_value_rank(value: &LogicScalarValue) -> u8 {
454    match value {
455        LogicScalarValue::U32(_) => 0,
456        LogicScalarValue::U64(_) => 1,
457        LogicScalarValue::I32(_) => 2,
458        LogicScalarValue::I64(_) => 3,
459        LogicScalarValue::F32(_) => 4,
460        LogicScalarValue::F64(_) => 5,
461        LogicScalarValue::Bool(_) => 6,
462        LogicScalarValue::Symbol(_) => 7,
463    }
464}
465
466/// Result of evaluating an entire Datalog program.
467pub struct LogicEvalResult {
468    /// One result per `?-` query in the source program.
469    pub queries: Vec<LogicQueryResult>,
470    /// Execution statistics (populated when profiling is enabled).
471    pub stats: Option<ExecutionStats>,
472}
473
474/// Runtime state retained by a persistent logic session.
475pub struct LogicSessionRuntime {
476    reusable_state_identity: Arc<LogicProgramIdentity>,
477    executor: Executor,
478    profiling: bool,
479}
480
481struct ResidentEvaluationOwner {
482    provider: Arc<CudaKernelProvider>,
483    input_shape: Vec<(String, Schema, u64)>,
484    profiling: bool,
485    executor: Executor,
486    prepared: PreparedResidentGraph,
487}
488
489struct LogicProgramIdentity {
490    resident_certification:
491        OnceLock<std::result::Result<Arc<ResidentGraphCertifiedPlan>, Arc<str>>>,
492    resident_evaluation_owner: Mutex<Option<ResidentEvaluationOwner>>,
493    #[cfg(test)]
494    resident_certification_initializations: AtomicU64,
495}
496
497impl LogicProgramIdentity {
498    fn new() -> Self {
499        Self {
500            resident_certification: OnceLock::new(),
501            resident_evaluation_owner: Mutex::new(None),
502            #[cfg(test)]
503            resident_certification_initializations: AtomicU64::new(0),
504        }
505    }
506
507    fn take_resident_evaluation_owner(
508        &self,
509        provider: &Arc<CudaKernelProvider>,
510        input_shape: &[(String, Schema, u64)],
511        profiling: bool,
512    ) -> Result<Option<ResidentEvaluationOwner>> {
513        let mut cached = self.resident_evaluation_owner.lock().map_err(|_| {
514            XlogError::Execution("resident evaluation owner lock is poisoned".into())
515        })?;
516        let matches = cached.as_ref().is_some_and(|owner| {
517            Arc::ptr_eq(&owner.provider, provider)
518                && owner.input_shape == input_shape
519                && owner.profiling == profiling
520        });
521        let owner = cached.take();
522        drop(cached);
523        Ok(owner.filter(|_| matches))
524    }
525
526    fn retain_resident_evaluation_owner(&self, owner: ResidentEvaluationOwner) -> Result<()> {
527        let mut cached = self.resident_evaluation_owner.lock().map_err(|_| {
528            XlogError::Execution("resident evaluation owner lock is poisoned".into())
529        })?;
530        let previous = cached.replace(owner);
531        drop(cached);
532        drop(previous);
533        Ok(())
534    }
535
536    fn get_or_init_resident_certification(
537        &self,
538        initialize: impl FnOnce() -> Result<ResidentGraphCertifiedPlan>,
539    ) -> Result<Arc<ResidentGraphCertifiedPlan>> {
540        let cached = self.resident_certification.get_or_init(|| {
541            #[cfg(test)]
542            self.resident_certification_initializations
543                .fetch_add(1, Ordering::Relaxed);
544            initialize()
545                .map(Arc::new)
546                .map_err(|error| Arc::<str>::from(error.to_string()))
547        });
548        cached.as_ref().map(Arc::clone).map_err(|message| {
549            XlogError::Execution(format!("resident route certification failed: {message}"))
550        })
551    }
552
553    fn get_or_init_resident_certification_with_outcome(
554        &self,
555        initialize: impl FnOnce() -> Result<ResidentGraphCertifiedPlan>,
556    ) -> Result<(Arc<ResidentGraphCertifiedPlan>, bool, bool)> {
557        let cache_was_warm = self.resident_certification.get().is_some();
558        let mut initialized_here = false;
559        let cached = self.resident_certification.get_or_init(|| {
560            initialized_here = true;
561            #[cfg(test)]
562            self.resident_certification_initializations
563                .fetch_add(1, Ordering::Relaxed);
564            initialize()
565                .map(Arc::new)
566                .map_err(|error| Arc::<str>::from(error.to_string()))
567        });
568        cached
569            .as_ref()
570            .map(|certified| (Arc::clone(certified), cache_was_warm, initialized_here))
571            .map_err(|message| {
572                XlogError::Execution(format!("resident route certification failed: {message}"))
573            })
574    }
575
576    #[cfg(test)]
577    fn resident_certification_initializations(&self) -> u64 {
578        self.resident_certification_initializations
579            .load(Ordering::Relaxed)
580    }
581}
582
583/// A materialized derived store produced by one compiled logic program.
584///
585/// The store's program identity is intentionally opaque. It can be inspected
586/// read-only, but only the originating [`LogicProgram`] (or one of its clones)
587/// can accept it as reusable execution state.
588pub struct LogicMaterializedStore {
589    reusable_state_identity: Arc<LogicProgramIdentity>,
590    store: RelationStore,
591}
592
593impl LogicMaterializedStore {
594    /// Borrow the materialized relations for read-only result inspection.
595    pub fn as_relation_store(&self) -> &RelationStore {
596        &self.store
597    }
598}
599
600impl LogicSessionRuntime {
601    /// Return persistent hash-index cache telemetry for the retained executor.
602    pub fn join_index_cache_stats(&self) -> JoinIndexCacheStats {
603        self.executor.join_index_cache_stats()
604    }
605
606    /// Return multiway/Free-Join dispatch telemetry for the retained executor.
607    pub fn wcoj_dispatch_stats(&self) -> WcojDispatchStats {
608        WcojDispatchStats {
609            free_join_dispatch_count: self.executor.free_join_dispatch_count(),
610            factorized_delta_dispatch_count: self.executor.factorized_delta_dispatch_count(),
611            wcoj_groupby_fusion_dispatch_count: self.executor.wcoj_groupby_fusion_dispatch_count(),
612            wcoj_error_decline_count: self.executor.wcoj_error_decline_count(),
613            wcoj_fallback: self.executor.wcoj_fallback_stats(),
614        }
615    }
616}
617
618/// Multiway/Free-Join dispatch telemetry counters for a retained session
619/// executor. Counts accumulate across evaluates within the session.
620#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
621pub struct WcojDispatchStats {
622    /// Free Join dispatches taken through the multiway plan.
623    pub free_join_dispatch_count: u64,
624    /// Factorized recursive-delta dispatches taken in the semi-naive
625    /// fixpoint (dense bitvector or sparse hash-set route).
626    pub factorized_delta_dispatch_count: u64,
627    /// Aggregate-fused group-by-root dispatches (no materialized join rows).
628    pub wcoj_groupby_fusion_dispatch_count: u64,
629    /// WCOJ pipeline errors that declined to the binary-join fallback.
630    pub wcoj_error_decline_count: u64,
631    /// Actual fallbacks classified by the route that was attempted.
632    pub wcoj_fallback: xlog_runtime::WcojFallbackStats,
633}
634
635/// Planner-grade telemetry for a persistent-session relation delta update.
636#[derive(Clone, Debug, Default, PartialEq)]
637pub struct DeltaPlannerTelemetry {
638    /// True when the relation-delta path reused an existing session/cache.
639    pub cache_reused: bool,
640    /// Planner decision used for this delta update.
641    pub fallback_decision: String,
642    /// Number of SCCs affected by the delta dependency closure.
643    pub affected_sccs: usize,
644    /// Number of SCCs recomputed from scratch.
645    pub recomputed_sccs: usize,
646    /// Number of SCCs updated incrementally.
647    pub incremental_sccs: usize,
648    /// Estimated speedup of delta evaluation over full recompute when available.
649    pub estimated_delta_speedup: Option<f64>,
650    /// Measured speedup of delta evaluation over full recompute when both timings are available.
651    pub measured_delta_speedup: Option<f64>,
652    /// Human-readable planner guidance for downstream diagnostics.
653    pub planner_advice: Vec<String>,
654}
655
656impl DeltaPlannerTelemetry {
657    /// Build planner telemetry from a delta report and optional timing evidence.
658    pub fn from_delta_report(
659        report: &LogicDeltaReport,
660        cache_reused: bool,
661        measured_micros: Option<(u64, u64)>,
662    ) -> Self {
663        let fallback_decision = if report.affected_sccs == 0 {
664            "no_op"
665        } else if report.has_deletes || report.recomputed_sccs > 0 {
666            "full_recompute_fallback"
667        } else {
668            "incremental"
669        }
670        .to_string();
671        let estimated_delta_speedup = if report.affected_sccs > 0 {
672            Some((report.affected_sccs.max(1) as f64) / (report.incremental_sccs.max(1) as f64))
673        } else {
674            None
675        };
676        let measured_delta_speedup = measured_micros.and_then(|(delta_us, full_us)| {
677            if delta_us == 0 {
678                None
679            } else {
680                Some(full_us as f64 / delta_us as f64)
681            }
682        });
683
684        let mut planner_advice = Vec::new();
685        if fallback_decision == "full_recompute_fallback" {
686            planner_advice.push(
687                "full recompute fallback selected; inspect deletes or affected SCC fanout"
688                    .to_string(),
689            );
690        } else if let Some(speedup) = measured_delta_speedup {
691            if speedup >= 1.0 {
692                planner_advice.push(format!("delta path is faster by {speedup:.2}x"));
693            } else {
694                planner_advice.push(format!(
695                    "full recompute may be faster; delta measured {speedup:.2}x"
696                ));
697            }
698        } else if fallback_decision == "incremental" {
699            planner_advice.push(
700                "incremental delta path selected; run equivalence timing to measure speedup"
701                    .to_string(),
702            );
703        }
704
705        Self {
706            cache_reused,
707            fallback_decision,
708            affected_sccs: report.affected_sccs,
709            recomputed_sccs: report.recomputed_sccs,
710            incremental_sccs: report.incremental_sccs,
711            estimated_delta_speedup,
712            measured_delta_speedup,
713            planner_advice,
714        }
715    }
716}
717
718/// Summary for a persistent-session relation delta update.
719pub struct LogicDeltaReport {
720    /// Number of relation delta entries supplied by the caller before coalescing.
721    pub input_delta_count: usize,
722    /// Number of changed relation names in the delta batch.
723    pub changed_relations: usize,
724    /// Changed relation names after coalescing.
725    pub changed_relation_names: Vec<String>,
726    /// Total inserted rows across all changed relations.
727    pub insert_rows: u64,
728    /// Total deleted rows across all changed relations.
729    pub delete_rows: u64,
730    /// True when at least one relation supplied delete rows.
731    pub has_deletes: bool,
732    /// Number of SCCs whose dependency closure was affected.
733    pub affected_sccs: usize,
734    /// Number of affected SCCs that were cleared and fully recomputed.
735    pub recomputed_sccs: usize,
736    /// Number of affected SCCs updated without clearing prior output.
737    pub incremental_sccs: usize,
738    /// Net insert rows after batch coalescing and insert/delete cancellation.
739    pub coalesced_insert_rows: u64,
740    /// Net delete rows after batch coalescing and insert/delete cancellation.
741    pub coalesced_delete_rows: u64,
742    /// Rows canceled because an insert and delete for the same relation matched in the batch.
743    pub canceled_rows: u64,
744    /// Planner-grade cache, fallback, and speedup telemetry.
745    pub planner_telemetry: DeltaPlannerTelemetry,
746    /// Metadata-only debug trace for the delta recompute.
747    pub debug_trace: Vec<String>,
748}
749
750/// Direction of an incoming relation-delta occurrence.
751#[derive(Clone, Copy, Debug, PartialEq, Eq)]
752pub enum RelationDeltaDirection {
753    /// Tuples supplied on the insertion side of an update.
754    Insert,
755    /// Tuples supplied on the deletion side of an update.
756    Delete,
757}
758
759/// Tuples canceled at one ordered merge step of a relation-delta batch.
760pub struct RelationDeltaCancellation {
761    update_index: usize,
762    incoming_direction: RelationDeltaDirection,
763    tuples: CudaBuffer,
764}
765
766impl RelationDeltaCancellation {
767    /// Return the zero-based position of the incoming update in the original batch.
768    pub fn update_index(&self) -> usize {
769        self.update_index
770    }
771
772    /// Return the direction of the incoming occurrence that caused the cancellation.
773    pub fn incoming_direction(&self) -> RelationDeltaDirection {
774        self.incoming_direction
775    }
776
777    /// Borrow the GPU-resident tuples canceled at this merge step.
778    pub fn tuples(&self) -> &CudaBuffer {
779        &self.tuples
780    }
781}
782
783#[derive(Clone, Copy)]
784struct PreparedRelationDeltaReportSeed {
785    input_delta_count: usize,
786    changed_relations: usize,
787    coalesced_insert_rows: u64,
788    coalesced_delete_rows: u64,
789    canceled_rows: u64,
790}
791
792/// Device-coalesced relation updates prepared for validation and later application.
793///
794/// Dropping this value releases its GPU-resident net-delta and cancellation
795/// buffers without changing any relation store.
796#[must_use = "prepared relation deltas have no effect until they are committed"]
797pub struct PreparedRelationDeltaBatch {
798    deltas: HashMap<String, RelationDelta>,
799    cancellations: HashMap<String, Vec<RelationDeltaCancellation>>,
800    report_seed: PreparedRelationDeltaReportSeed,
801}
802
803impl PreparedRelationDeltaBatch {
804    /// Borrow the final net relation deltas produced by the device coalescer.
805    pub fn net_deltas(&self) -> &HashMap<String, RelationDelta> {
806        &self.deltas
807    }
808
809    /// Borrow per-relation cancellation traces in global update order.
810    pub fn cancellations(&self) -> &HashMap<String, Vec<RelationDeltaCancellation>> {
811        &self.cancellations
812    }
813
814    fn into_application_parts(
815        self,
816    ) -> (
817        HashMap<String, RelationDelta>,
818        PreparedRelationDeltaReportSeed,
819    ) {
820        (self.deltas, self.report_seed)
821    }
822}
823
824/// A fully staged relation update bound to its authoritative and derived state.
825///
826/// The exclusive borrows prevent callers from mutating or substituting the
827/// authoritative store, cache, or runtime between preparation and commit.
828/// Dropping this value discards every staged update and prospective derived
829/// state. The authoritative store remains unchanged, while the borrowed cache
830/// and runtime slots remain empty because their prior values were consumed by
831/// preparation.
832///
833/// A prepared commit has no API for selecting a different destination:
834///
835/// ```compile_fail
836/// use xlog_gpu::logic::PreparedRelationDeltaCommit;
837/// use xlog_runtime::RelationStore;
838///
839/// fn commit_into_another_store(
840///     prepared: PreparedRelationDeltaCommit<'_>,
841///     other: &mut RelationStore,
842/// ) {
843///     prepared.commit(other);
844/// }
845/// ```
846///
847/// The source store also remains exclusively borrowed until commit:
848///
849/// ```compile_fail
850/// use std::sync::Arc;
851/// use xlog_core::Result;
852/// use xlog_cuda::{CudaBuffer, CudaKernelProvider};
853/// use xlog_gpu::logic::{
854///     LogicMaterializedStore, LogicProgram, LogicSessionRuntime, PreparedRelationDeltaBatch,
855/// };
856/// use xlog_runtime::RelationStore;
857///
858/// fn mutate_after_prepare(
859///     program: &LogicProgram,
860///     provider: Arc<CudaKernelProvider>,
861///     store: &mut RelationStore,
862///     cache: &mut Option<LogicMaterializedStore>,
863///     runtime: &mut Option<LogicSessionRuntime>,
864///     batch: PreparedRelationDeltaBatch,
865///     replacement: CudaBuffer,
866/// ) -> Result<()> {
867///     let prepared = program.prepare_relation_delta_commit_with_session_runtime(
868///         provider, store, cache, runtime, batch,
869///     )?;
870///     store.put("fact", replacement);
871///     prepared.commit();
872///     Ok(())
873/// }
874/// ```
875#[must_use = "dropping a prepared commit discards its staged relation updates"]
876pub struct PreparedRelationDeltaCommit<'a> {
877    provider: Arc<CudaKernelProvider>,
878    authoritative_relation_store: &'a mut RelationStore,
879    cached_store_slot: &'a mut Option<LogicMaterializedStore>,
880    session_runtime_slot: &'a mut Option<LogicSessionRuntime>,
881    staged_base_updates: Vec<(String, CudaBuffer)>,
882    prospective_cached_store: Option<LogicMaterializedStore>,
883    prospective_session_runtime: Option<LogicSessionRuntime>,
884    report: LogicDeltaReport,
885}
886
887impl PreparedRelationDeltaCommit<'_> {
888    /// Borrow the prospective materialized derived/cache store.
889    ///
890    /// This store is suitable for direct query-result comparison. It must not
891    /// seed an independent full recompute because it contains intensional heads
892    /// that an executor may union with newly derived rows. For a no-op batch it
893    /// returns retained derived state when available, or the unchanged
894    /// authoritative store otherwise.
895    pub fn prospective_derived_store(&self) -> &RelationStore {
896        if let Some(store) = self.prospective_cached_store.as_ref() {
897            return &store.store;
898        }
899        if let Some(runtime) = self.prospective_session_runtime.as_ref() {
900            return runtime.executor.store();
901        }
902        &*self.authoritative_relation_store
903    }
904
905    /// Clone the authoritative base snapshot with every staged base update overlaid.
906    ///
907    /// This fallible, on-demand snapshot is the correct seed for an independent
908    /// full recompute. It includes staged relations that were absent from the
909    /// authoritative store and does not mutate authoritative contents or
910    /// versions. Ordinary prepare and commit paths do not pay for these clones.
911    pub fn clone_prospective_base_store(&self) -> Result<RelationStore> {
912        let mut authoritative_names = self
913            .authoritative_relation_store
914            .names()
915            .filter(|name| {
916                !self
917                    .staged_base_updates
918                    .iter()
919                    .any(|(staged_name, _)| staged_name == name)
920            })
921            .collect::<Vec<_>>();
922        authoritative_names.sort_unstable();
923        let mut cloned = RelationStore::new(self.provider.clone());
924        cloned.try_reserve_relations(authoritative_names.len() + self.staged_base_updates.len())?;
925
926        for name in authoritative_names {
927            let buffer = self.authoritative_relation_store.get(name).ok_or_else(|| {
928                XlogError::Execution(format!(
929                    "Authoritative relation {name} disappeared while cloning prospective base state"
930                ))
931            })?;
932            let context = format!("cloning prospective base relation '{name}'");
933            let cloned_buffer = self
934                .provider
935                .clone_buffer(buffer)
936                .map_err(|error| relation_clone_error(context, error))?;
937            cloned.put(name, cloned_buffer);
938        }
939
940        for (name, buffer) in &self.staged_base_updates {
941            let context = format!("cloning staged prospective base relation '{name}'");
942            let cloned_buffer = self
943                .provider
944                .clone_buffer(buffer)
945                .map_err(|error| relation_clone_error(context, error))?;
946            cloned.put(name, cloned_buffer);
947        }
948        Ok(cloned)
949    }
950
951    /// Install every staged base update and derived-state replacement together.
952    ///
953    /// All allocation, destination-capacity reservation, recomputation,
954    /// constraint validation, and buffer cloning has completed before this
955    /// method is available, so committing only moves owned values and is
956    /// infallible.
957    pub fn commit(self) -> LogicDeltaReport {
958        for (name, buffer) in self.staged_base_updates {
959            self.authoritative_relation_store.put_owned(name, buffer);
960        }
961        *self.cached_store_slot = self.prospective_cached_store;
962        *self.session_runtime_slot = self.prospective_session_runtime;
963        self.report
964    }
965}
966
967#[derive(Default)]
968struct PendingRelationDelta {
969    insert: Option<CudaBuffer>,
970    delete: Option<CudaBuffer>,
971}
972
973/// One stratum of a stratified epistemic plan: the epistemic head(s) it
974/// materializes plus the GPU executable plan that computes them.
975///
976/// Lower strata are executed first; their GATED head outputs are written into the
977/// relation store as base relations BEFORE higher strata run, so a higher
978/// stratum's `know`/`possible` over a lower head gates against the materialized
979/// (now-base) relation through the existing tuple-key membership filter.
980#[derive(Clone)]
981struct StratumExecutable {
982    /// The stratum's GPU plan: single-head or joint multi-head split. The gated
983    /// head relation name(s) are recovered from the plan's reductions at runtime.
984    plan: StratumPlanKind,
985}
986
987#[derive(Clone)]
988struct StratifiedExecutable {
989    strata: Vec<StratumExecutable>,
990    /// Single ordinary closure and authored-constraint stage executed only after
991    /// every modal stratum has materialized its gated heads.
992    ordinary_post: GpuOrdinaryPass,
993}
994
995#[derive(Clone)]
996enum StratumPlanKind {
997    Single(Box<EpistemicExecutablePlan>),
998    Split(Box<EpistemicSplitExecutablePlan>),
999    /// A higher stratum that RECURSES over a lower stratum's materialized
1000    /// (now-base) determined head. Once the determined head is a base relation in
1001    /// the store, its `know`/`possible` modal is over an invariant relation, so the
1002    /// stratum is admissible Case-A: the modal resolves to an ordinary join (no
1003    /// second gate) and the recursive semi-naive engine iterates the fixpoint. The
1004    /// reduced ordinary program drives an ordinary RIR plan whose head IS this
1005    /// stratum's user-visible output relation.
1006    Ordinary {
1007        plan: Box<ExecutionPlan>,
1008        /// User-visible output head predicate(s) this stratum computes.
1009        head_predicates: Vec<String>,
1010    },
1011}
1012
1013#[derive(Clone)]
1014enum LogicExecutionPlan {
1015    Ordinary(Box<ExecutionPlan>),
1016    EpistemicG91Compatibility(Box<EpistemicG91CompatibilityGpuPlan>),
1017    EpistemicWfsGpu(Box<EpistemicWfsGpuPlan>),
1018    EpistemicSingle(Box<EpistemicExecutablePlan>),
1019    EpistemicSplit(Box<EpistemicSplitExecutablePlan>),
1020    /// Stratified epistemic execution: ordered strata, each materializing its
1021    /// gated head(s) into the store before the next stratum runs.
1022    EpistemicStratified(Box<StratifiedExecutable>),
1023}
1024
1025#[derive(Clone)]
1026struct EpistemicG91CompatibilityGpuPlan {
1027    upper_bound: GpuEvaluationPass,
1028    refinement: GpuEvaluationPass,
1029    snapshot_relations: BTreeMap<String, String>,
1030    convergence_predicates: Vec<String>,
1031    max_iterations: usize,
1032}
1033
1034#[derive(Clone)]
1035enum GpuEvaluationPass {
1036    Ordinary(Box<GpuOrdinaryPass>),
1037    Wfs(Box<EpistemicWfsGpuPlan>),
1038}
1039
1040#[derive(Clone)]
1041struct EpistemicWfsGpuPlan {
1042    overapprox: GpuOrdinaryPass,
1043    lower: GpuOrdinaryPass,
1044    upper: GpuOrdinaryPass,
1045    intensional_predicates: Vec<String>,
1046    upper_fixed_names: HashMap<String, String>,
1047    lower_fixed_names: HashMap<String, String>,
1048    max_iterations: usize,
1049}
1050
1051#[derive(Clone)]
1052struct GpuOrdinaryPass {
1053    plan: ExecutionPlan,
1054    schemas: HashMap<String, Schema>,
1055    rel_ids: HashMap<String, RelId>,
1056}
1057
1058/// Compile-time epistemic provenance, retained even when the executable plan is
1059/// `Ordinary` (e.g. a Case-A recursive epistemic fixpoint whose modal literals were
1060/// resolved into invariant joins). This carries the source's epistemic literals so
1061/// the epistemic plan dump can emit a stable id for a recursive epistemic fixpoint that no
1062/// longer carries an epistemic GPU plan.
1063#[derive(Clone)]
1064struct EpistemicProvenance {
1065    /// How the epistemic source was reduced for execution.
1066    reduction: &'static str,
1067    /// Epistemic `know`/`possible` literals (with negation) seen in the source EIR.
1068    literals: Vec<xlog_ir::EirEpistemicLiteral>,
1069    /// Whether ordinary-plan query results retain the source epistemic relation name
1070    /// and logical zero-column shape instead of exposing compiler projection details.
1071    surface_source_queries: bool,
1072}
1073
1074/// A compiled Datalog program ready for GPU evaluation.
1075#[derive(Clone)]
1076pub struct LogicProgram {
1077    reusable_state_identity: Arc<LogicProgramIdentity>,
1078    /// Merged authored program retained for public diagnostics and result labels.
1079    source_program: Program,
1080    /// Normalized or reduced program used by compilation and execution.
1081    program: Program,
1082    /// Integrity constraints as authored before normalization, retained only when
1083    /// their order matches the source-order constraint provenance used by the plan.
1084    authored_constraints: Option<Vec<Constraint>>,
1085    plan: LogicExecutionPlan,
1086    schemas: HashMap<String, Schema>,
1087    rel_ids: HashMap<String, RelId>,
1088    fact_arity_qualifications: OnceLock<BTreeSet<String>>,
1089    /// `Some` iff the source program contained epistemic literals (regardless of
1090    /// whether the executable plan ended up epistemic or ordinary).
1091    epistemic_provenance: Option<EpistemicProvenance>,
1092}
1093
1094/// A self-contained XLOG program whose three selected zero-arity queries define
1095/// a semantic task's positive support. Compilation preserves the authored
1096/// source; observation executes the canonical GPU evaluator before deriving any
1097/// result. Presence is `True` and absence is `Neither`, never negative support.
1098/// Other native observers may produce `False` or `Both`; all four values share
1099/// the same task-result contract. Source facts are the complete concrete inputs,
1100/// and query ordinals retain the caller's order. No caller-supplied answers enter
1101/// this adapter.
1102pub struct SemanticLogicTaskProgram {
1103    source: String,
1104    query_ordinals: [usize; 3],
1105    program: LogicProgram,
1106    editable: Option<Arc<xlog_cuda::SemanticProgramAdmission>>,
1107}
1108
1109impl std::fmt::Debug for SemanticLogicTaskProgram {
1110    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1111        formatter
1112            .debug_struct("SemanticLogicTaskProgram")
1113            .field("source", &self.source)
1114            .field("query_ordinals", &self.query_ordinals)
1115            .finish_non_exhaustive()
1116    }
1117}
1118
1119impl SemanticLogicTaskProgram {
1120    /// Compile a complete source program and select three zero-arity query results.
1121    /// This performs no GPU execution; the native semantic owner calls `observe`
1122    /// during cold binding, including when restoring a retained task.
1123    pub fn compile(source: String, query_ordinals: [usize; 3]) -> Result<Self> {
1124        let program = LogicProgram::compile(&source)?;
1125        if !program.source_program.imports.is_empty() {
1126            return Err(XlogError::Compilation(
1127                "semantic task source must contain its complete program without imports".into(),
1128            ));
1129        }
1130        for ordinal in query_ordinals {
1131            let query = program.source_program.queries.get(ordinal).ok_or_else(|| {
1132                XlogError::Compilation(format!("semantic task query ordinal {ordinal} is absent"))
1133            })?;
1134            if !query_output_vars(query).is_empty() {
1135                return Err(XlogError::Compilation(format!(
1136                    "semantic task query ordinal {ordinal} must have a zero-arity result",
1137                )));
1138            }
1139        }
1140        Ok(Self {
1141            source,
1142            query_ordinals,
1143            program,
1144            editable: None,
1145        })
1146    }
1147
1148    /// Bind the independently admitted editable source to the observer without
1149    /// deriving candidate syntax or order from the observer's expected answers.
1150    pub fn with_editable_program(
1151        mut self,
1152        editable: Arc<xlog_cuda::SemanticProgramAdmission>,
1153    ) -> Self {
1154        self.editable = Some(editable);
1155        self
1156    }
1157}
1158
1159/// Lower an admitted editable source to the exact positive binary program bank
1160/// consumed by the resident semantic evaluator. The independent observer is
1161/// deliberately absent: it may supply expected truth, never candidate syntax.
1162pub fn compile_positive_binary_task(
1163    initial_theory: &str,
1164    input_facts: &str,
1165    statements: [&str; 3],
1166) -> Result<xlog_cuda::SemanticProgramAdmission> {
1167    use xlog_cuda::{SemanticProgramAdmission, SemanticProgramFact};
1168
1169    let theory = xlog_logic::parse_program(initial_theory)?;
1170    let facts = xlog_logic::parse_program(input_facts)?;
1171    if !positive_binary_source_only(&theory)
1172        || !positive_binary_source_only(&facts)
1173        || !facts.predicates.is_empty()
1174        || facts.rules.iter().any(|rule| !rule.is_fact())
1175    {
1176        return Err(XlogError::Compilation(
1177            "editable semantic source requires declarations, positive binary rules and separate ground facts only".into(),
1178        ));
1179    }
1180    // The production compiler remains the authority for XLOG syntax and type
1181    // semantics. The resident lowering below only handles its admitted subset.
1182    let mut executable_source = String::with_capacity(
1183        initial_theory.len()
1184            + input_facts.len()
1185            + statements.iter().map(|s| s.len() + 5).sum::<usize>()
1186            + 5,
1187    );
1188    executable_source.push_str(initial_theory);
1189    executable_source.push('\n');
1190    executable_source.push_str(input_facts);
1191    executable_source.push('\n');
1192    for statement in statements {
1193        executable_source.push_str("?- ");
1194        executable_source.push_str(statement);
1195        executable_source.push_str(".\n");
1196    }
1197    LogicProgram::compile(&executable_source)?;
1198
1199    let mut predicate_indices = BTreeMap::new();
1200    for (index, declaration) in theory.predicates.iter().enumerate() {
1201        if declaration.types.len() != 2
1202            || declaration
1203                .types
1204                .iter()
1205                .any(|typ| *typ != xlog_logic::ast::TypeRef::Scalar(ScalarType::U32))
1206            || predicate_indices
1207                .insert(declaration.name.clone(), index as u32)
1208                .is_some()
1209        {
1210            return Err(XlogError::Compilation(
1211                "editable semantic predicates must be distinct binary u32 declarations".into(),
1212            ));
1213        }
1214    }
1215    if predicate_indices.is_empty() {
1216        return Err(XlogError::Compilation(
1217            "editable semantic source has no declared predicates".into(),
1218        ));
1219    }
1220
1221    let mut initial_facts = Vec::new();
1222    let mut initial_rules = Vec::new();
1223    for rule in theory.rules.iter().chain(&facts.rules) {
1224        if rule.is_fact() {
1225            initial_facts.push(lower_binary_fact(&rule.head, &predicate_indices)?);
1226        } else {
1227            initial_rules.push(lower_positive_binary_rule(rule, &predicate_indices)?);
1228        }
1229    }
1230    let parsed_queries = xlog_logic::parse_program(&executable_source)?;
1231    let queries: [SemanticProgramFact; 3] = parsed_queries
1232        .queries
1233        .iter()
1234        .map(|query| lower_binary_fact(&query.atom, &predicate_indices))
1235        .collect::<Result<Vec<_>>>()?
1236        .try_into()
1237        .map_err(|_| {
1238            XlogError::Compilation("editable semantic task requires three ground queries".into())
1239        })?;
1240    Ok(SemanticProgramAdmission {
1241        predicate_count: theory.predicates.len() as u32,
1242        initial_facts,
1243        initial_rules,
1244        queries,
1245    })
1246}
1247
1248fn positive_binary_source_only(program: &Program) -> bool {
1249    program.imports.is_empty()
1250        && program.functions.is_empty()
1251        && program.domains.is_empty()
1252        && program.constraints.is_empty()
1253        && program.authored_constraint_source_bound.is_none()
1254        && program.queries.is_empty()
1255        && program.prob_facts.is_empty()
1256        && program.annotated_disjunctions.is_empty()
1257        && program.evidence.is_empty()
1258        && program.prob_queries.is_empty()
1259        && program.neural_predicates.is_empty()
1260        && program.learnable_rules.is_empty()
1261        && program.directives == Default::default()
1262}
1263
1264fn lower_binary_fact(
1265    atom: &Atom,
1266    predicates: &BTreeMap<String, u32>,
1267) -> Result<xlog_cuda::SemanticProgramFact> {
1268    use xlog_cuda::SemanticProgramFact;
1269    let predicate = *predicates.get(&atom.predicate).ok_or_else(|| {
1270        XlogError::Compilation(format!("undeclared editable predicate {}", atom.predicate))
1271    })?;
1272    if atom.terms.len() != 2 {
1273        return Err(XlogError::Compilation(
1274            "editable fact must be binary".into(),
1275        ));
1276    }
1277    let value = |term: &Term| match term {
1278        Term::Integer(value) => u32::try_from(*value).map_err(|_| {
1279            XlogError::Compilation("editable fact requires a u32 ground value".into())
1280        }),
1281        _ => Err(XlogError::Compilation(
1282            "editable fact requires a u32 ground value".into(),
1283        )),
1284    };
1285    Ok(SemanticProgramFact {
1286        predicate,
1287        first: value(&atom.terms[0])?,
1288        second: value(&atom.terms[1])?,
1289    })
1290}
1291
1292fn lower_positive_binary_rule(
1293    rule: &Rule,
1294    predicates: &BTreeMap<String, u32>,
1295) -> Result<xlog_cuda::SemanticProgramRule> {
1296    use xlog_cuda::SemanticProgramRule;
1297    if !(1..=2).contains(&rule.body.len()) || rule.head.terms.len() != 2 {
1298        return Err(XlogError::Compilation(
1299            "editable rule requires one or two positive binary body atoms".into(),
1300        ));
1301    }
1302    let predicate = |atom: &Atom| -> Result<u32> {
1303        if atom.terms.len() != 2 {
1304            return Err(XlogError::Compilation(
1305                "editable rule atom must be binary".into(),
1306            ));
1307        }
1308        predicates.get(&atom.predicate).copied().ok_or_else(|| {
1309            XlogError::Compilation(format!("undeclared editable predicate {}", atom.predicate))
1310        })
1311    };
1312    let mut names = BTreeMap::new();
1313    let mut next = 0u32;
1314    let mut body = Vec::with_capacity(rule.body.len());
1315    for literal in &rule.body {
1316        let BodyLiteral::Positive(atom) = literal else {
1317            return Err(XlogError::Compilation(
1318                "editable rule body must contain positive atoms only".into(),
1319            ));
1320        };
1321        let mut variables = [0; 2];
1322        for (position, term) in atom.terms.iter().enumerate().take(2) {
1323            variables[position] = match term {
1324                Term::Variable(name) => *names.entry(name.clone()).or_insert_with(|| {
1325                    let value = next;
1326                    next += 1;
1327                    value
1328                }),
1329                Term::Anonymous => {
1330                    let value = next;
1331                    next += 1;
1332                    value
1333                }
1334                _ => {
1335                    return Err(XlogError::Compilation(
1336                        "editable rule body requires variables only".into(),
1337                    ))
1338                }
1339            };
1340        }
1341        body.push((predicate(atom)?, variables));
1342    }
1343    let mut head_variables = [0; 2];
1344    for (position, term) in rule.head.terms.iter().enumerate() {
1345        let Term::Variable(name) = term else {
1346            return Err(XlogError::Compilation(
1347                "editable rule head requires bound variables".into(),
1348            ));
1349        };
1350        head_variables[position] = *names.get(name).ok_or_else(|| {
1351            XlogError::Compilation("editable rule head variable is unbound".into())
1352        })?;
1353    }
1354    Ok(SemanticProgramRule {
1355        head_predicate: predicate(&rule.head)?,
1356        left_predicate: body[0].0,
1357        right_predicate: body.get(1).map_or(0, |item| item.0),
1358        body_count: body.len() as u32,
1359        head_variables,
1360        left_variables: body[0].1,
1361        right_variables: body.get(1).map_or([0, 0], |item| item.1),
1362    })
1363}
1364
1365#[cfg(test)]
1366mod semantic_program_lowering_tests {
1367    use super::compile_positive_binary_task;
1368    use xlog_cuda::{SemanticProgramFact, SemanticProgramRule};
1369
1370    #[test]
1371    fn admitted_source_lowers_base_rules_facts_and_ordered_queries() {
1372        let theory = "pred p(u32, u32).\npred q(u32, u32).\npred t(u32, u32).\n\
1373                      t(X, Y) :- q(X, Y).\n";
1374        let facts = "p(1, 2).\nq(6, 7).\np(4, 5).\n";
1375        let queries = ["t(1, 3)", "t(6, 7)", "t(4, 3)"];
1376        let admission = compile_positive_binary_task(theory, facts, queries).unwrap();
1377        assert_eq!(admission.predicate_count, 3);
1378        assert_eq!(admission.initial_facts.len(), 3);
1379        assert_eq!(admission.initial_rules.len(), 1);
1380        assert_eq!(
1381            admission.queries[0],
1382            SemanticProgramFact {
1383                predicate: 2,
1384                first: 1,
1385                second: 3
1386            }
1387        );
1388        assert_eq!(
1389            admission.initial_rules[0],
1390            SemanticProgramRule {
1391                head_predicate: 2,
1392                left_predicate: 1,
1393                right_predicate: 0,
1394                body_count: 1,
1395                head_variables: [0, 1],
1396                left_variables: [0, 1],
1397                right_variables: [0, 0],
1398            }
1399        );
1400    }
1401
1402    #[test]
1403    fn separate_rule_and_fact_sources_preserve_missing_composition() {
1404        let theory = "pred p(u32, u32).\npred q(u32, u32).\npred r(u32, u32).\n\
1405                      pred u(u32, u32).\npred t(u32, u32).\n\
1406                      u(X, Y) :- p(X, Y).\nu(X, Y) :- r(X, Y).\nt(X, Y) :- r(X, Y).\n";
1407        let facts = "p(1, 2).\np(4, 5).\nq(2, 3).\nq(1, 4).\nr(6, 7).\n";
1408        let admission =
1409            compile_positive_binary_task(theory, facts, ["t(1, 3)", "t(1, 4)", "t(4, 3)"]).unwrap();
1410        assert_eq!(admission.predicate_count, 5);
1411        assert_eq!(admission.initial_facts.len(), 5);
1412        assert_eq!(admission.initial_rules.len(), 3);
1413        assert_eq!(admission.queries[0].predicate, 4);
1414        assert_eq!(admission.queries[0].first, 1);
1415        assert_eq!(admission.queries[0].second, 3);
1416        assert!(admission
1417            .initial_rules
1418            .iter()
1419            .all(|rule| rule.body_count == 1));
1420        assert!(!admission.initial_rules.iter().any(|rule| {
1421            rule.head_predicate == 4 && rule.left_predicate == 0 && rule.right_predicate == 1
1422        }));
1423    }
1424
1425    #[test]
1426    fn unsupported_or_unsafe_source_cannot_become_partial_program() {
1427        let declaration = "pred p(u32, u32).\npred t(u32, u32).\n";
1428        let queries = ["t(1, 2)", "t(2, 3)", "t(3, 4)"];
1429        assert!(compile_positive_binary_task(
1430            &(declaration.to_owned() + "t(X, Z) :- p(X, Y).\n"),
1431            "p(1, 2).\n",
1432            queries,
1433        )
1434        .is_err());
1435        assert!(compile_positive_binary_task(
1436            &(declaration.to_owned() + "t(X, Y) :- not p(X, Y).\n"),
1437            "p(1, 2).\n",
1438            queries,
1439        )
1440        .is_err());
1441        assert!(compile_positive_binary_task(
1442            declaration,
1443            "p(1, 2).\nt(X, Y) :- p(X, Y).\n",
1444            queries,
1445        )
1446        .is_err());
1447        assert!(compile_positive_binary_task(
1448            declaration,
1449            "p(1, 2).\n",
1450            ["t(1, 2)", "t(X, 2)", "t(3, 4)"],
1451        )
1452        .is_err());
1453    }
1454}
1455
1456impl xlog_cuda::SemanticTaskProgram for SemanticLogicTaskProgram {
1457    fn editable_program(&self) -> Option<&xlog_cuda::SemanticProgramAdmission> {
1458        self.editable.as_deref()
1459    }
1460
1461    fn observe(
1462        &self,
1463        provider: Arc<CudaKernelProvider>,
1464    ) -> std::result::Result<xlog_cuda::SemanticTaskObservation, xlog_cuda::SemanticTransitionError>
1465    {
1466        let execution_error = |error: XlogError| xlog_cuda::SemanticTransitionError::InvalidInput {
1467            detail: format!("native semantic task program: {error}"),
1468        };
1469        let result = self
1470            .program
1471            .evaluate(Arc::clone(&provider), HashMap::new())
1472            .map_err(execution_error)?;
1473        // Query counts may already be cached on the host. Complete every
1474        // executor stream explicitly before treating any result as observed;
1475        // the semantic owner's cold binding poisons the session on failure.
1476        let context = provider.device().inner().stream().context();
1477        context.bind_to_thread().map_err(|error| {
1478            execution_error(XlogError::Kernel(format!(
1479                "task observer context binding failed: {error}"
1480            )))
1481        })?;
1482        context.synchronize().map_err(|error| {
1483            execution_error(XlogError::Kernel(format!(
1484                "task observer completion failed: {error}"
1485            )))
1486        })?;
1487        let mut input_bytes = b"xlog.semantic-task.query-selection.v1\0".to_vec();
1488        let mut result_bytes = b"xlog.semantic-task.four-valued-results.v1\0".to_vec();
1489        let mut expected_truth = [xlog_cuda::SemanticTruth::Neither; 3];
1490        for (slot, ordinal) in self.query_ordinals.into_iter().enumerate() {
1491            let query = result.queries.get(ordinal).ok_or_else(|| {
1492                execution_error(XlogError::Execution(format!(
1493                    "semantic task query ordinal {ordinal} was not returned"
1494                )))
1495            })?;
1496            if query.buffer.schema().arity() != 0 {
1497                return Err(execution_error(XlogError::Execution(format!(
1498                    "semantic task query ordinal {ordinal} returned a nonzero-arity relation",
1499                ))));
1500            }
1501            // This is the canonical completed-result boundary. Device validity
1502            // and logical row count are checked before the cold result is used;
1503            // no observation or host transfer occurs in the transition hot loop.
1504            let rows = provider
1505                .validated_logical_row_count(&query.buffer)
1506                .map_err(execution_error)?;
1507            if rows > 1 {
1508                return Err(execution_error(XlogError::Execution(format!(
1509                    "semantic task query ordinal {ordinal} returned multiple zero-arity rows",
1510                ))));
1511            }
1512            input_bytes.extend_from_slice(&(ordinal as u64).to_le_bytes());
1513            expected_truth[slot] = if rows == 1 {
1514                xlog_cuda::SemanticTruth::True
1515            } else {
1516                xlog_cuda::SemanticTruth::Neither
1517            };
1518            result_bytes.extend_from_slice(&(expected_truth[slot] as u64).to_le_bytes());
1519        }
1520        Ok(xlog_cuda::SemanticTaskObservation {
1521            program_source: self.source.as_bytes().to_vec(),
1522            input_bytes,
1523            result_bytes,
1524            expected_truth,
1525        })
1526    }
1527}
1528
1529/// Read-only metadata for one argument of a compiled relation.
1530#[derive(Clone, Debug, PartialEq, Eq)]
1531pub struct LogicArgumentSchema {
1532    name: String,
1533    source_named: bool,
1534    sort: Option<String>,
1535    scalar_type: ScalarType,
1536}
1537
1538impl LogicArgumentSchema {
1539    /// Return the compiled column name.
1540    pub fn name(&self) -> &str {
1541        &self.name
1542    }
1543
1544    /// Return whether the column name was written in the source declaration.
1545    pub fn source_named(&self) -> bool {
1546        self.source_named
1547    }
1548
1549    /// Return the source domain alias, when the argument used one.
1550    pub fn sort(&self) -> Option<&str> {
1551        self.sort.as_deref()
1552    }
1553
1554    /// Return the resolved scalar type from the compiled relation schema.
1555    pub fn scalar_type(&self) -> ScalarType {
1556        self.scalar_type
1557    }
1558}
1559
1560impl LogicProgram {
1561    /// Compile a Datalog source string into a GPU-executable program.
1562    pub fn compile(source: &str) -> Result<Self> {
1563        let program = xlog_logic::parse_program(source)?;
1564        Self::compile_program(program)
1565    }
1566
1567    /// Compile an entry file and its complete transitive module closure.
1568    pub fn compile_file(entry_file: &Path, search_paths: Vec<PathBuf>) -> Result<Self> {
1569        let resolver = xlog_logic::compile::load_modules(entry_file, search_paths)
1570            .map_err(|error| XlogError::Compilation(error.to_string()))?;
1571        let entry_program = resolver
1572            .entry()
1573            .ok_or_else(|| XlogError::Compilation("module resolver has no entry program".into()))?
1574            .program
1575            .clone();
1576        let merged = resolver
1577            .merge_imports(entry_program)
1578            .map_err(|error| XlogError::Compilation(error.to_string()))?;
1579        Self::compile_program(merged)
1580    }
1581
1582    /// Compile an already parsed program into a GPU-executable program.
1583    ///
1584    /// This method does not resolve imports; import-aware callers merge them
1585    /// first. The program enters the canonical execution normalizer once here.
1586    pub fn compile_program(mut program: Program) -> Result<Self> {
1587        if program.authored_constraint_source_bound.is_some() {
1588            program.validate_prepared_authored_constraint_identity()?;
1589        } else {
1590            program.prepare_authored_constraint_identity_at_root()?;
1591        }
1592        let source_program = program.clone();
1593        let normalized = normalize_program_for_execution(program)?;
1594        Self::compile_normalized_program(normalized, source_program)
1595    }
1596
1597    fn compile_normalized_program(normalized: Program, source_program: Program) -> Result<Self> {
1598        for query_index in 0..normalized.queries.len() {
1599            let generated_head = format!("__xlog_query_{query_index}");
1600            let authored_collision = source_program
1601                .predicates
1602                .iter()
1603                .any(|declaration| declaration.name == generated_head)
1604                || source_program
1605                    .rules
1606                    .iter()
1607                    .any(|rule| rule.head.predicate == generated_head);
1608            if authored_collision {
1609                return Err(XlogError::Compilation(format!(
1610                    "authored relation {generated_head} collides with generated query head"
1611                )));
1612            }
1613        }
1614        // Function, meta-term, list, and shared-variable normalization preserve
1615        // constraint count and source order. Keep the authored snapshot only
1616        // while that one-to-one invariant remains observable.
1617        let authored_constraints = (source_program.constraints.len()
1618            == normalized.constraints.len())
1619        .then(|| source_program.constraints.clone());
1620        let reusable_state_identity = Arc::new(LogicProgramIdentity::new());
1621        let compiled = if program_has_epistemic_literals(&normalized) {
1622            Self::compile_epistemic_program(
1623                normalized,
1624                source_program,
1625                authored_constraints,
1626                reusable_state_identity,
1627            )?
1628        } else {
1629            let mut compiler = Compiler::new();
1630            let plan = match qualify_same_name_multi_arity_program(&normalized) {
1631                Some(qualified) => compiler.compile_prepared_program(&qualified)?,
1632                None => compiler.compile_prepared_program(&normalized)?,
1633            };
1634            let mut schemas = compiler.schemas().clone();
1635            augment_same_name_multi_arity_schemas(&normalized, &mut schemas)?;
1636            Self {
1637                reusable_state_identity,
1638                source_program,
1639                program: normalized,
1640                authored_constraints,
1641                plan: LogicExecutionPlan::Ordinary(Box::new(plan)),
1642                schemas,
1643                rel_ids: compiler.rel_ids().clone(),
1644                fact_arity_qualifications: OnceLock::new(),
1645                epistemic_provenance: None,
1646            }
1647        };
1648        Ok(compiled.finalize_compilation())
1649    }
1650
1651    fn finalize_compilation(self) -> Self {
1652        if !self.program.queries.is_empty() {
1653            if let LogicExecutionPlan::Ordinary(plan) = &self.plan {
1654                let _ = self.resident_certified_plan_for_plan(plan);
1655            }
1656        }
1657        self
1658    }
1659
1660    fn validate_reusable_state_identity(
1661        &self,
1662        state_identity: &Arc<LogicProgramIdentity>,
1663        state_name: &str,
1664    ) -> Result<()> {
1665        if Arc::ptr_eq(&self.reusable_state_identity, state_identity) {
1666            return Ok(());
1667        }
1668        Err(XlogError::Execution(format!(
1669            "{state_name} belongs to a different compiled logic program"
1670        )))
1671    }
1672
1673    fn validate_reusable_state_slots(
1674        &self,
1675        cached_store: Option<&LogicMaterializedStore>,
1676        session_runtime: Option<&LogicSessionRuntime>,
1677    ) -> Result<()> {
1678        if let Some(cached_store) = cached_store {
1679            self.validate_reusable_state_identity(
1680                &cached_store.reusable_state_identity,
1681                "materialized cache",
1682            )?;
1683        }
1684        if let Some(session_runtime) = session_runtime {
1685            self.validate_reusable_state_identity(
1686                &session_runtime.reusable_state_identity,
1687                "session runtime",
1688            )?;
1689        }
1690        Ok(())
1691    }
1692
1693    fn bind_materialized_store(&self, store: RelationStore) -> LogicMaterializedStore {
1694        LogicMaterializedStore {
1695            reusable_state_identity: self.reusable_state_identity.clone(),
1696            store,
1697        }
1698    }
1699
1700    fn compile_epistemic_program(
1701        normalized: Program,
1702        source_program: Program,
1703        authored_constraints: Option<Vec<Constraint>>,
1704        reusable_state_identity: Arc<LogicProgramIdentity>,
1705    ) -> Result<Self> {
1706        // Capture epistemic provenance up front: the source-EIR modal literals are
1707        // retained even when a Case-A recursive reduction lowers the program to an
1708        // Ordinary executable plan, so the epistemic plan dump can still emit a stable id
1709        // for a recursive epistemic fixpoint.
1710        let provenance_literals = collect_eir_epistemic_literals(&normalized);
1711        let prepared = prepare_epistemic_program(&normalized)?;
1712        let active_program = prepared.active_program();
1713
1714        // Positive Gelfond-1991 `possible` cycles require tuple-level dynamic
1715        // compatibility. Predicate SCC membership alone cannot prove that the same
1716        // concrete tuple survives every edge's ordinary body filters. Route the
1717        // complete active program through a descending GPU fixpoint before any
1718        // stratified or ordinary least-fixpoint reduction can erase those gates.
1719        if let Some(reduction) = try_prepare_g91_compatibility_reduction(&prepared)? {
1720            let plan = compile_g91_compatibility_gpu_plan(&reduction)?;
1721            let schemas = g91_plan_combined_schemas(&plan);
1722            let rel_ids = g91_plan_combined_rel_ids(&plan);
1723            return Ok(Self {
1724                reusable_state_identity,
1725                source_program,
1726                program: reduction.refinement_program().clone(),
1727                authored_constraints,
1728                plan: LogicExecutionPlan::EpistemicG91Compatibility(Box::new(plan)),
1729                schemas,
1730                rel_ids,
1731                fact_arity_qualifications: OnceLock::new(),
1732                epistemic_provenance: Some(EpistemicProvenance {
1733                    reduction: "g91_tuple_compatibility",
1734                    literals: provenance_literals,
1735                    surface_source_queries: true,
1736                }),
1737            });
1738        }
1739
1740        // Stratified epistemic execution FIRST: a modal literal ranges over an
1741        // epistemically-DETERMINED derived head (`b :- know a` where `a :- know p`,
1742        // `p` invariant — possibly with the higher stratum RECURSING over the
1743        // determined head, e.g. `reach :- reach, know a`). Partition into strata;
1744        // each is compiled through the existing epistemic OR Case-A ordinary path,
1745        // and at runtime each lower stratum's GATED head is materialized into the
1746        // store as a base relation before the higher stratum gates against it (via
1747        // the existing tuple-key membership filter or — once the head is a materialized
1748        // base relation — Case-A resolve-into-body; either way NO double-gating
1749        // against a still-modal relation). A shared BASE modal `q` (EDB, not a
1750        // determined derived head) returns `None` here and falls through to
1751        // the joint path UNCHANGED; plain Case-A recursion over an EDB modal
1752        // (`know edge`) also returns `None` and falls through to Case-A below.
1753        if let Some(stratified) = try_plan_stratified_epistemic_program(active_program)? {
1754            // SCHEMA-ONLY reduction: resolve augmenting positive modals over INVARIANT
1755            // *or* epistemically-DETERMINED targets into positive ordinary atoms, so an
1756            // augmented head whose extra output column is bound by a modal over a
1757            // multi-column determined head (`out(X) :- node(X), know r(X, Y)`, `r`
1758            // determined) types its appended `Y` column from `r`'s declaration instead
1759            // of failing closed as `UnsafeVariable`. This drives ONLY plan schema
1760            // inference; per-stratum EXECUTION compiles below over sub-programs where
1761            // the determined head is already a materialized base relation (strict
1762            // invariant resolve), so no modal is ever resolved over an un-gated
1763            // candidate at runtime.
1764            let reduced =
1765                reduce_epistemic_program_to_ordinary_for_stratified_schema(active_program)?;
1766            let mut schema_compiler = Compiler::new();
1767            schema_compiler.compile_prepared_program(&reduced)?;
1768            let mut schemas = schema_compiler.schemas().clone();
1769            augment_same_name_multi_arity_schemas(active_program, &mut schemas)?;
1770
1771            let mut strata = Vec::with_capacity(stratified.strata.len());
1772            for stratum in &stratified.strata {
1773                strata.push(StratumExecutable {
1774                    plan: Self::compile_stratum_plan(&stratum.program)?,
1775                });
1776            }
1777            let ordinary_post = compile_gpu_ordinary_pass(&stratified.ordinary_post_program)?;
1778            for (name, schema) in &ordinary_post.schemas {
1779                schemas
1780                    .entry(name.clone())
1781                    .or_insert_with(|| schema.clone());
1782            }
1783            let plan = LogicExecutionPlan::EpistemicStratified(Box::new(StratifiedExecutable {
1784                strata,
1785                ordinary_post,
1786            }));
1787            let rel_ids = epistemic_relation_ids(&plan)?;
1788            return Ok(Self {
1789                reusable_state_identity,
1790                source_program,
1791                program: normalized,
1792                authored_constraints,
1793                plan,
1794                schemas,
1795                rel_ids,
1796                fact_arity_qualifications: OnceLock::new(),
1797                epistemic_provenance: Some(EpistemicProvenance {
1798                    reduction: "stratified",
1799                    literals: provenance_literals,
1800                    surface_source_queries: true,
1801                }),
1802            });
1803        }
1804
1805        // Case A/B: reduce admissible recursive epistemic programs to ordinary
1806        // recursion. Stratified reduced programs route through the existing ordinary
1807        // semi-naive engine; non-monotone reduced SCCs route through the GPU-native
1808        // WFS alternating-fixpoint plan below. Recursive shapes outside the admissible
1809        // fragment still fail closed in `try_reduce_case_a_recursive_epistemic_program`.
1810        if let Some(recursive_reduced) = try_reduce_prepared_recursive_epistemic_program(&prepared)?
1811        {
1812            let strat = xlog_logic::stratify::analyze_stratification(&recursive_reduced);
1813            if !strat.non_monotone_sccs.is_empty() {
1814                let wfs_plan = compile_epistemic_wfs_gpu_plan(&recursive_reduced)?;
1815                let schemas = wfs_plan_combined_schemas(&wfs_plan);
1816                let rel_ids = wfs_plan_combined_rel_ids(&wfs_plan);
1817                return Ok(Self {
1818                    reusable_state_identity,
1819                    source_program,
1820                    program: recursive_reduced,
1821                    authored_constraints,
1822                    plan: LogicExecutionPlan::EpistemicWfsGpu(Box::new(wfs_plan)),
1823                    schemas,
1824                    rel_ids,
1825                    fact_arity_qualifications: OnceLock::new(),
1826                    epistemic_provenance: Some(EpistemicProvenance {
1827                        reduction: "wfs_gpu_recursive",
1828                        literals: provenance_literals,
1829                        surface_source_queries: true,
1830                    }),
1831                });
1832            }
1833            let mut compiler = Compiler::new();
1834            let plan = compiler.compile_prepared_program(&recursive_reduced)?;
1835            return Ok(Self {
1836                reusable_state_identity,
1837                source_program,
1838                program: recursive_reduced,
1839                authored_constraints,
1840                plan: LogicExecutionPlan::Ordinary(Box::new(plan)),
1841                schemas: compiler.schemas().clone(),
1842                rel_ids: compiler.rel_ids().clone(),
1843                fact_arity_qualifications: OnceLock::new(),
1844                epistemic_provenance: Some(EpistemicProvenance {
1845                    reduction: "ordinary_recursive_modal_reduction",
1846                    literals: provenance_literals,
1847                    surface_source_queries: true,
1848                }),
1849            });
1850        }
1851
1852        let reduced = reduce_epistemic_program_to_ordinary(active_program)?;
1853        let mut schema_compiler = Compiler::new();
1854        schema_compiler.compile_prepared_program(&reduced)?;
1855        let mut schemas = schema_compiler.schemas().clone();
1856        augment_same_name_multi_arity_schemas(active_program, &mut schemas)?;
1857
1858        let plan = if epistemic_output_head_predicate_count(active_program) > 1 {
1859            LogicExecutionPlan::EpistemicSplit(Box::new(compile_epistemic_gpu_split_execution(
1860                active_program,
1861            )?))
1862        } else {
1863            match compile_epistemic_gpu_execution(active_program) {
1864                Ok(executable) => LogicExecutionPlan::EpistemicSingle(Box::new(executable)),
1865                Err(XlogError::UnsupportedEpistemicConstruct { construct, .. })
1866                    if construct == "epistemic GPU final output relation" =>
1867                {
1868                    LogicExecutionPlan::EpistemicSplit(Box::new(
1869                        compile_epistemic_gpu_split_execution(active_program)?,
1870                    ))
1871                }
1872                Err(err) => return Err(err),
1873            }
1874        };
1875        let rel_ids = epistemic_relation_ids(&plan)?;
1876        Ok(Self {
1877            reusable_state_identity,
1878            source_program,
1879            program: normalized,
1880            authored_constraints,
1881            plan,
1882            schemas,
1883            rel_ids,
1884            fact_arity_qualifications: OnceLock::new(),
1885            epistemic_provenance: Some(EpistemicProvenance {
1886                reduction: "epistemic_executable",
1887                literals: provenance_literals,
1888                surface_source_queries: false,
1889            }),
1890        })
1891    }
1892
1893    /// Compile one stratum sub-program into its plan kind.
1894    ///
1895    /// A stratum whose epistemic heads gate only over invariant or
1896    /// already-materialized lower-stratum relations is either an admissible Case-A
1897    /// recursion (the modal resolves to an ordinary join over the now-base relation)
1898    /// or a plain single/joint epistemic plan. Case-A is tried first so a recursive
1899    /// higher stratum (`reach :- reach, know a`, `a` materialized base) routes
1900    /// through the ordinary semi-naive engine.
1901    fn compile_stratum_plan(stratum_program: &Program) -> Result<StratumPlanKind> {
1902        if let Some(case_a_reduced) =
1903            try_reduce_case_a_recursive_epistemic_program(stratum_program)?
1904        {
1905            let mut compiler = Compiler::new();
1906            let plan = compiler.compile_prepared_program(&case_a_reduced)?;
1907            let head_predicates = epistemic_stratum_output_heads(stratum_program);
1908            return Ok(StratumPlanKind::Ordinary {
1909                plan: Box::new(plan),
1910                head_predicates,
1911            });
1912        }
1913        if epistemic_output_head_predicate_count(stratum_program) > 1 {
1914            Ok(StratumPlanKind::Split(Box::new(
1915                compile_epistemic_gpu_split_execution(stratum_program)?,
1916            )))
1917        } else {
1918            Ok(StratumPlanKind::Single(Box::new(
1919                compile_epistemic_gpu_execution(stratum_program)?,
1920            )))
1921        }
1922    }
1923
1924    /// Compile a program with module resolution.
1925    ///
1926    /// This method resolves all imports using the provided resolver and merges
1927    /// imported predicates, functions, and rules into the main program.
1928    ///
1929    /// Pragmas are entry-file-scoped: directives declared in imported modules
1930    /// are dropped at merge time. This library entry point does not surface
1931    /// them — embedders that want the CLI's `warning[W0510]` behavior should
1932    /// call `resolver.ignored_import_pragmas()` before compiling and report
1933    /// the returned records on their own diagnostics channel.
1934    ///
1935    /// # Arguments
1936    /// * `source` - The source code of the main program
1937    /// * `resolver` - A pre-loaded ModuleResolver with all dependencies resolved
1938    ///
1939    /// # Returns
1940    /// The compiled LogicProgram with all imports merged
1941    pub fn compile_with_resolver(
1942        source: &str,
1943        resolver: &xlog_logic::resolver::ModuleResolver,
1944    ) -> Result<Self> {
1945        let program = xlog_logic::parse_program(source)?;
1946
1947        // Merge imports from the resolver
1948        let merged = resolver
1949            .merge_imports(program)
1950            .map_err(|e| XlogError::Compilation(format!("Module resolution failed: {}", e)))?;
1951
1952        Self::compile_program(merged)
1953    }
1954
1955    /// Serialize the compiled epistemic execution plan to a JSON summary.
1956    ///
1957    /// Returns `None` for ordinary (non-epistemic) programs. For epistemic
1958    /// programs this dumps the EIR-derived GPU plan(s): selected mode, the
1959    /// epistemic `know`/`possible` literals (with negation), required GPU hot-path
1960    /// phases/kernels, world-view integrity constraints, reduced-program head
1961    /// summaries, the fail-closed GPU execution policy, and a deterministic plan
1962    /// id (a stable hash of the canonical summary). Runtime evidence separately
1963    /// records observed dispatch, kernel, device-buffer, candidate-accounting,
1964    /// solver/probability event, and scoped-transfer behavior.
1965    pub fn epistemic_plan_json(&self) -> Option<String> {
1966        let mut has_ordinary_post = false;
1967        let gpu_plans: Vec<(String, &xlog_ir::EpistemicGpuPlan)> = match &self.plan {
1968            // A program whose source was epistemic but whose executable plan is
1969            // ordinary either resolved admissible recursive modal literals into joins
1970            // or removed every unfounded FAEEL modal rule. It carries no epistemic GPU
1971            // plan and executes through the ordinary GPU engine under the same
1972            // reject-unsupported policy. Emit a provenance summary with a stable id so the reduction is
1973            // auditable.
1974            LogicExecutionPlan::Ordinary(_) => {
1975                let prov = self.epistemic_provenance.as_ref()?;
1976                return Some(epistemic_provenance_summary_json(
1977                    "epistemic_reduced_ordinary",
1978                    prov,
1979                    None,
1980                    None,
1981                ));
1982            }
1983            LogicExecutionPlan::EpistemicWfsGpu(wfs) => {
1984                let prov = self.epistemic_provenance.as_ref()?;
1985                return Some(epistemic_provenance_summary_json(
1986                    self.plan_kind_label(),
1987                    prov,
1988                    Some(wfs.max_iterations),
1989                    Some(wfs),
1990                ));
1991            }
1992            LogicExecutionPlan::EpistemicG91Compatibility(g91) => {
1993                let prov = self.epistemic_provenance.as_ref()?;
1994                return Some(g91_compatibility_summary_json(
1995                    self.plan_kind_label(),
1996                    prov,
1997                    g91,
1998                ));
1999            }
2000            LogicExecutionPlan::EpistemicSingle(plan) => {
2001                vec![("single".to_string(), &plan.gpu_plan)]
2002            }
2003            LogicExecutionPlan::EpistemicSplit(split) => split
2004                .components
2005                .iter()
2006                .enumerate()
2007                .map(|(i, c)| (format!("split[{i}]"), &c.executable.gpu_plan))
2008                .collect(),
2009            LogicExecutionPlan::EpistemicStratified(stratified) => {
2010                let mut plans = Vec::new();
2011                for (i, stratum) in stratified.strata.iter().enumerate() {
2012                    match &stratum.plan {
2013                        StratumPlanKind::Single(plan) => {
2014                            plans.push((format!("stratum[{i}]"), &plan.gpu_plan));
2015                        }
2016                        StratumPlanKind::Split(split) => {
2017                            for (j, c) in split.components.iter().enumerate() {
2018                                plans.push((
2019                                    format!("stratum[{i}].split[{j}]"),
2020                                    &c.executable.gpu_plan,
2021                                ));
2022                            }
2023                        }
2024                        // Recursive/ordinary higher strata carry no epistemic GPU
2025                        // plan (the modal already resolved to an ordinary join over
2026                        // a materialized base); the enclosing summary carries the
2027                        // same fail-closed GPU execution policy.
2028                        StratumPlanKind::Ordinary { .. } => {}
2029                    }
2030                }
2031                has_ordinary_post = true;
2032                plans
2033            }
2034        };
2035        Some(epistemic_plan_summary_json(
2036            self.plan_kind_label(),
2037            &gpu_plans,
2038            has_ordinary_post,
2039        ))
2040    }
2041
2042    fn plan_kind_label(&self) -> &'static str {
2043        match &self.plan {
2044            LogicExecutionPlan::Ordinary(_) => "ordinary",
2045            LogicExecutionPlan::EpistemicG91Compatibility(_) => "epistemic_g91_compatibility_gpu",
2046            LogicExecutionPlan::EpistemicWfsGpu(_) => "epistemic_wfs_gpu",
2047            LogicExecutionPlan::EpistemicSingle(_) => "epistemic_single",
2048            LogicExecutionPlan::EpistemicSplit(_) => "epistemic_split",
2049            LogicExecutionPlan::EpistemicStratified(_) => "epistemic_stratified",
2050        }
2051    }
2052
2053    /// Look up the schema for a named relation.
2054    pub fn schema(&self, relation: &str) -> Option<&Schema> {
2055        self.schemas.get(relation)
2056    }
2057
2058    /// Return the full schema map (relation name to schema).
2059    pub fn schemas(&self) -> &HashMap<String, Schema> {
2060        &self.schemas
2061    }
2062
2063    /// Return ordered argument metadata for a compiled relation.
2064    ///
2065    /// Column names and scalar types come from the compiled schema. Source
2066    /// declarations additionally preserve whether each name was explicit and
2067    /// which domain alias, if any, supplied its scalar type.
2068    pub fn argument_schema(&self, relation: &str) -> Option<Vec<LogicArgumentSchema>> {
2069        let schema = self.schemas.get(relation)?;
2070        let presentation_program = self.presentation_program();
2071        let source_declaration = presentation_program
2072            .predicates
2073            .iter()
2074            .rev()
2075            .find(|decl| arity_qualified_name(&decl.name, decl.arity()) == relation)
2076            .or_else(|| {
2077                presentation_program
2078                    .predicates
2079                    .iter()
2080                    .rev()
2081                    .find(|decl| decl.name == relation)
2082            });
2083        let source_columns = source_declaration.map(|declaration| declaration.schema_columns());
2084
2085        Some(
2086            schema
2087                .columns
2088                .iter()
2089                .enumerate()
2090                .map(|(index, (name, scalar_type))| {
2091                    let source_column = source_columns
2092                        .as_ref()
2093                        .and_then(|columns| columns.get(index));
2094                    LogicArgumentSchema {
2095                        name: name.clone(),
2096                        source_named: source_column
2097                            .and_then(|column| column.name.as_ref())
2098                            .is_some(),
2099                        sort: source_column.and_then(|column| match &column.typ {
2100                            TypeRef::Domain(name) => Some(name.clone()),
2101                            _ => None,
2102                        }),
2103                        scalar_type: *scalar_type,
2104                    }
2105                })
2106                .collect(),
2107        )
2108    }
2109
2110    /// Return stable rule provenance for source-visible rules.
2111    pub fn rule_provenance(&self) -> Vec<xlog_logic::RuleProvenance> {
2112        xlog_logic::source_diagnostics(&self.source_program, &self.program, None).0
2113    }
2114
2115    /// Return direct proof traces for source queries.
2116    pub fn proof_traces(&self) -> Vec<xlog_logic::QueryProofTrace> {
2117        xlog_logic::source_diagnostics(&self.source_program, &self.program, None).1
2118    }
2119
2120    fn presentation_program(&self) -> &Program {
2121        &self.source_program
2122    }
2123
2124    /// Create a persistent user-visible relation store initialized with inline facts.
2125    pub fn create_relation_store(
2126        &self,
2127        provider: Arc<CudaKernelProvider>,
2128    ) -> Result<RelationStore> {
2129        let mut store = RelationStore::new(provider.clone());
2130        for (name, schema) in &self.schemas {
2131            if is_user_visible_relation(name) || is_list_helper_relation(name) {
2132                store.put(name, provider.create_empty_buffer(schema.clone())?);
2133            }
2134        }
2135        self.load_facts_into_store(provider.as_ref(), &mut store)?;
2136        Ok(store)
2137    }
2138
2139    /// Evaluate using a persistent base relation store.
2140    ///
2141    /// The provided store is treated as immutable seed state. Buffers are cloned
2142    /// into a fresh executor for each evaluation so repeated evaluations reuse
2143    /// stored relations without mutating the persistent store itself.
2144    pub fn evaluate_with_relation_store(
2145        &self,
2146        provider: Arc<CudaKernelProvider>,
2147        relation_store: &RelationStore,
2148        profiling: bool,
2149    ) -> Result<LogicEvalResult> {
2150        let (result, _) =
2151            self.evaluate_with_relation_store_and_cache(provider, relation_store, profiling)?;
2152        Ok(result)
2153    }
2154
2155    /// Evaluate using a persistent relation store and return the complete runtime store.
2156    pub fn evaluate_with_relation_store_and_cache(
2157        &self,
2158        provider: Arc<CudaKernelProvider>,
2159        relation_store: &RelationStore,
2160        profiling: bool,
2161    ) -> Result<(LogicEvalResult, LogicMaterializedStore)> {
2162        self.reject_compiler_generated_query_relation_names(
2163            relation_store.names(),
2164            "persistent caller",
2165        )?;
2166        let resident_mode = ResidentSelectionMode::from_env()?;
2167        let mut executor =
2168            self.executor_from_materialized_store(provider.clone(), relation_store, profiling)?;
2169        executor.execute_plan(self.ordinary_plan("relation-store evaluation")?)?;
2170        self.enforce_constraints(&provider, &executor)?;
2171
2172        let mut stats = if profiling {
2173            let total_output_rows = self.total_query_rows(provider.as_ref(), executor.store())?;
2174            Some(executor.execution_stats(total_output_rows))
2175        } else {
2176            None
2177        };
2178        if resident_mode.enabled() {
2179            if resident_mode == ResidentSelectionMode::Require {
2180                return Err(XlogError::Execution(
2181                    "resident conditional-graph execution was required, but complete-store evaluation requires the existing GPU path"
2182                        .to_string(),
2183                ));
2184            }
2185            if let Some(stats) = stats.as_mut() {
2186                stats.resident_graph = Some(ResidentGraphExecutionStats::declined(
2187                    ResidentGraphDeclineReason::FullStoreRequested,
2188                ));
2189            }
2190        }
2191
2192        let cached_store = self.clone_relation_store(&provider, executor.store())?;
2193        let result = self.logic_result_from_store(provider.as_ref(), &cached_store, stats)?;
2194        Ok((result, self.bind_materialized_store(cached_store)))
2195    }
2196
2197    /// Create retained runtime state for a persistent relation session.
2198    pub fn create_session_runtime(
2199        &self,
2200        provider: Arc<CudaKernelProvider>,
2201        relation_store: &RelationStore,
2202        profiling: bool,
2203    ) -> Result<LogicSessionRuntime> {
2204        self.reject_compiler_generated_query_relation_names(
2205            relation_store.names(),
2206            "persistent caller",
2207        )?;
2208        self.ordinary_plan("persistent relation session")?;
2209        let executor =
2210            self.executor_from_materialized_store(provider, relation_store, profiling)?;
2211        Ok(LogicSessionRuntime {
2212            reusable_state_identity: self.reusable_state_identity.clone(),
2213            executor,
2214            profiling,
2215        })
2216    }
2217
2218    fn create_session_runtime_from_materialized_store(
2219        &self,
2220        provider: Arc<CudaKernelProvider>,
2221        relation_store: &LogicMaterializedStore,
2222        profiling: bool,
2223    ) -> Result<LogicSessionRuntime> {
2224        self.validate_reusable_state_identity(
2225            &relation_store.reusable_state_identity,
2226            "materialized cache",
2227        )?;
2228        self.ordinary_plan("materialized relation session")?;
2229        Ok(LogicSessionRuntime {
2230            reusable_state_identity: self.reusable_state_identity.clone(),
2231            executor: self.executor_from_materialized_store(
2232                provider,
2233                &relation_store.store,
2234                profiling,
2235            )?,
2236            profiling,
2237        })
2238    }
2239
2240    /// Evaluate with retained session runtime state and return a materialized store snapshot.
2241    pub fn evaluate_with_session_runtime(
2242        &self,
2243        provider: Arc<CudaKernelProvider>,
2244        runtime: &mut LogicSessionRuntime,
2245    ) -> Result<(LogicEvalResult, LogicMaterializedStore)> {
2246        self.validate_reusable_state_identity(&runtime.reusable_state_identity, "session runtime")?;
2247        let resident_mode = ResidentSelectionMode::from_env()?;
2248        runtime.executor.set_profiling(runtime.profiling);
2249        runtime
2250            .executor
2251            .execute_plan(self.ordinary_plan("session runtime evaluation")?)?;
2252        self.enforce_constraints(&provider, &runtime.executor)?;
2253
2254        let mut stats = if runtime.profiling {
2255            let total_output_rows =
2256                self.total_query_rows(provider.as_ref(), runtime.executor.store())?;
2257            Some(runtime.executor.execution_stats(total_output_rows))
2258        } else {
2259            None
2260        };
2261        if resident_mode.enabled() {
2262            if resident_mode == ResidentSelectionMode::Require {
2263                return Err(XlogError::Execution(
2264                    "resident conditional-graph execution was required, but persistent session evaluation requires the existing GPU path"
2265                        .to_string(),
2266                ));
2267            }
2268            if let Some(stats) = stats.as_mut() {
2269                stats.resident_graph = Some(ResidentGraphExecutionStats::declined(
2270                    ResidentGraphDeclineReason::FullStoreRequested,
2271                ));
2272            }
2273        }
2274
2275        let cached_store = self.clone_relation_store(&provider, runtime.executor.store())?;
2276        let result = self.logic_result_from_store(provider.as_ref(), &cached_store, stats)?;
2277        Ok((result, self.bind_materialized_store(cached_store)))
2278    }
2279
2280    /// Build query results from an already materialized runtime store.
2281    ///
2282    /// A raw relation store cannot attest that it was materialized by this
2283    /// compiled program:
2284    ///
2285    /// ```compile_fail
2286    /// use std::sync::Arc;
2287    /// use xlog_core::Result;
2288    /// use xlog_cuda::CudaKernelProvider;
2289    /// use xlog_gpu::logic::LogicProgram;
2290    /// use xlog_runtime::RelationStore;
2291    ///
2292    /// fn evaluate_untrusted_store(
2293    ///     program: &LogicProgram,
2294    ///     provider: Arc<CudaKernelProvider>,
2295    ///     raw_store: &RelationStore,
2296    /// ) -> Result<()> {
2297    ///     program.evaluate_cached_relation_store(provider, raw_store)?;
2298    ///     Ok(())
2299    /// }
2300    /// ```
2301    pub fn evaluate_cached_relation_store(
2302        &self,
2303        provider: Arc<CudaKernelProvider>,
2304        relation_store: &LogicMaterializedStore,
2305    ) -> Result<LogicEvalResult> {
2306        self.validate_reusable_state_identity(
2307            &relation_store.reusable_state_identity,
2308            "materialized cache",
2309        )?;
2310        self.logic_result_from_store(provider.as_ref(), &relation_store.store, None)
2311    }
2312
2313    /// Apply relation deltas to a persistent session store through the runtime delta path.
2314    ///
2315    /// A cache from another compiled program is rejected without consuming it.
2316    /// After identity validation, an operational preparation failure leaves the
2317    /// authoritative relation store unchanged but discards the consumed cache.
2318    pub fn apply_relation_deltas(
2319        &self,
2320        provider: Arc<CudaKernelProvider>,
2321        relation_store: &mut RelationStore,
2322        cached_store: &mut Option<LogicMaterializedStore>,
2323        deltas: HashMap<String, RelationDelta>,
2324    ) -> Result<LogicDeltaReport> {
2325        self.validate_reusable_state_slots(cached_store.as_ref(), None)?;
2326        let mut session_runtime = None;
2327        let prepared = self.prepare_relation_delta_commit(
2328            provider,
2329            relation_store,
2330            cached_store,
2331            &mut session_runtime,
2332            deltas,
2333            None,
2334        )?;
2335        Ok(prepared.commit())
2336    }
2337
2338    /// Apply relation deltas while preserving retained session runtime state.
2339    ///
2340    /// State from another compiled program is rejected without consuming it.
2341    /// After identity validation, an operational preparation failure leaves the
2342    /// authoritative store unchanged and the derived-state slots empty.
2343    pub fn apply_relation_deltas_with_session_runtime(
2344        &self,
2345        provider: Arc<CudaKernelProvider>,
2346        relation_store: &mut RelationStore,
2347        cached_store: &mut Option<LogicMaterializedStore>,
2348        session_runtime: &mut Option<LogicSessionRuntime>,
2349        deltas: HashMap<String, RelationDelta>,
2350    ) -> Result<LogicDeltaReport> {
2351        self.validate_reusable_state_slots(cached_store.as_ref(), session_runtime.as_ref())?;
2352        let prepared = self.prepare_relation_delta_commit(
2353            provider,
2354            relation_store,
2355            cached_store,
2356            session_runtime,
2357            deltas,
2358            None,
2359        )?;
2360        Ok(prepared.commit())
2361    }
2362
2363    /// Prepare raw relation deltas without ordered-batch coalescing.
2364    ///
2365    /// This preserves the runtime's delete-then-insert semantics when one
2366    /// relation delta contains both directions. Callers that accept an ordered
2367    /// batch must use [`LogicProgram::prepare_relation_delta_batch`] exactly
2368    /// once and pass its result to
2369    /// [`LogicProgram::prepare_relation_delta_commit_with_session_runtime`].
2370    /// Foreign reusable state is rejected before it is consumed. Once identity
2371    /// validation succeeds, the current cache and runtime are consumed during
2372    /// preparation; on operational error their caller slots remain empty while
2373    /// the authoritative store remains unchanged.
2374    pub fn prepare_relation_deltas_commit_with_session_runtime<'a>(
2375        &self,
2376        provider: Arc<CudaKernelProvider>,
2377        relation_store: &'a mut RelationStore,
2378        cached_store: &'a mut Option<LogicMaterializedStore>,
2379        session_runtime: &'a mut Option<LogicSessionRuntime>,
2380        deltas: HashMap<String, RelationDelta>,
2381    ) -> Result<PreparedRelationDeltaCommit<'a>> {
2382        self.validate_reusable_state_slots(cached_store.as_ref(), session_runtime.as_ref())?;
2383        self.prepare_relation_delta_commit(
2384            provider,
2385            relation_store,
2386            cached_store,
2387            session_runtime,
2388            deltas,
2389            None,
2390        )
2391    }
2392
2393    /// Coalesce an ordered batch on the device and retain cancellation tuples
2394    /// for selected relations.
2395    pub fn prepare_relation_delta_batch(
2396        &self,
2397        provider: &CudaKernelProvider,
2398        delta_batch: Vec<(String, RelationDelta)>,
2399        cancellation_capture_relations: &BTreeSet<String>,
2400    ) -> Result<PreparedRelationDeltaBatch> {
2401        self.reject_compiler_generated_query_relation_names(
2402            delta_batch.iter().map(|(name, _)| name.as_str()),
2403            "caller delta",
2404        )?;
2405        coalesce_relation_delta_batch_with_cancellation_capture(
2406            provider,
2407            delta_batch,
2408            cancellation_capture_relations,
2409        )
2410    }
2411
2412    /// Build the prospective authoritative base store for an independent
2413    /// full-recompute diagnostic without consuming retained session state.
2414    ///
2415    /// This must run before preparing the retained-runtime commit. If cloning
2416    /// or applying a base delta fails, the caller's cache and session runtime
2417    /// remain available for subsequent evaluations.
2418    pub fn clone_prospective_base_for_prepared_delta_batch(
2419        &self,
2420        provider: &Arc<CudaKernelProvider>,
2421        authoritative_relation_store: &RelationStore,
2422        prepared_batch: &PreparedRelationDeltaBatch,
2423    ) -> Result<RelationStore> {
2424        self.reject_compiler_generated_query_relation_names(
2425            authoritative_relation_store.names(),
2426            "persistent caller",
2427        )?;
2428
2429        let deltas = prepared_batch.net_deltas();
2430        let mut unchanged_names = authoritative_relation_store
2431            .names()
2432            .filter(|name| !deltas.contains_key(*name))
2433            .collect::<Vec<_>>();
2434        unchanged_names.sort_unstable();
2435
2436        let mut changed_names = deltas.keys().map(String::as_str).collect::<Vec<_>>();
2437        changed_names.sort_unstable();
2438
2439        let mut prospective = RelationStore::new(provider.clone());
2440        prospective.try_reserve_relations(unchanged_names.len() + changed_names.len())?;
2441
2442        for name in unchanged_names {
2443            let buffer = authoritative_relation_store.get(name).ok_or_else(|| {
2444                XlogError::Execution(format!(
2445                    "Authoritative relation {name} disappeared while cloning prospective base state"
2446                ))
2447            })?;
2448            let context = format!("cloning prospective base relation '{name}'");
2449            let cloned = provider
2450                .clone_buffer(buffer)
2451                .map_err(|error| relation_clone_error(context, error))?;
2452            prospective.put(name, cloned);
2453        }
2454
2455        for name in changed_names {
2456            let delta = deltas.get(name).ok_or_else(|| {
2457                XlogError::Execution(format!(
2458                    "Prepared relation delta for {name} disappeared while cloning prospective base state"
2459                ))
2460            })?;
2461            let existing = authoritative_relation_store.get(name);
2462            let schema = existing
2463                .map(|buffer| buffer.schema().clone())
2464                .or_else(|| delta.insert.as_ref().map(|buffer| buffer.schema().clone()))
2465                .or_else(|| delta.delete.as_ref().map(|buffer| buffer.schema().clone()))
2466                .ok_or_else(|| {
2467                    XlogError::Execution(format!(
2468                        "Delta update for {name} has no existing relation and no schema"
2469                    ))
2470                })?;
2471            let context = format!("cloning prospective base relation '{name}'");
2472            let mut updated = match existing {
2473                Some(buffer) => provider
2474                    .clone_buffer(buffer)
2475                    .map_err(|error| relation_clone_error(context, error))?,
2476                None => provider.create_empty_buffer(schema)?,
2477            };
2478            if let Some(delete) = &delta.delete {
2479                updated = provider.diff_gpu(&updated, delete)?;
2480            }
2481            if let Some(insert) = &delta.insert {
2482                updated = provider.union_gpu(&updated, insert)?;
2483            }
2484            prospective.put(name, updated);
2485        }
2486
2487        Ok(prospective)
2488    }
2489
2490    /// Prepare a fully staged retained-runtime commit from a coalesced batch.
2491    ///
2492    /// Foreign reusable state is rejected before the coalesced batch is consumed.
2493    /// Once identity validation succeeds, the current runtime and cache move into
2494    /// the transaction. An operational preparation failure discards those values
2495    /// and leaves the caller slots empty; the authoritative store is unchanged.
2496    pub fn prepare_relation_delta_commit_with_session_runtime<'a>(
2497        &self,
2498        provider: Arc<CudaKernelProvider>,
2499        relation_store: &'a mut RelationStore,
2500        cached_store: &'a mut Option<LogicMaterializedStore>,
2501        session_runtime: &'a mut Option<LogicSessionRuntime>,
2502        prepared_batch: PreparedRelationDeltaBatch,
2503    ) -> Result<PreparedRelationDeltaCommit<'a>> {
2504        self.validate_reusable_state_slots(cached_store.as_ref(), session_runtime.as_ref())?;
2505        let (deltas, report_seed) = prepared_batch.into_application_parts();
2506        self.prepare_relation_delta_commit(
2507            provider,
2508            relation_store,
2509            cached_store,
2510            session_runtime,
2511            deltas,
2512            Some(report_seed),
2513        )
2514    }
2515
2516    fn prepare_relation_delta_commit<'a>(
2517        &self,
2518        provider: Arc<CudaKernelProvider>,
2519        relation_store: &'a mut RelationStore,
2520        cached_store: &'a mut Option<LogicMaterializedStore>,
2521        session_runtime: &'a mut Option<LogicSessionRuntime>,
2522        deltas: HashMap<String, RelationDelta>,
2523        report_seed: Option<PreparedRelationDeltaReportSeed>,
2524    ) -> Result<PreparedRelationDeltaCommit<'a>> {
2525        self.validate_reusable_state_slots(cached_store.as_ref(), session_runtime.as_ref())?;
2526        self.reject_compiler_generated_query_relation_names(
2527            relation_store.names(),
2528            "persistent caller",
2529        )?;
2530        self.reject_compiler_generated_query_relation_names(
2531            deltas.keys().map(String::as_str),
2532            "caller delta",
2533        )?;
2534        let insert_rows = deltas
2535            .values()
2536            .filter_map(|delta| delta.insert.as_ref())
2537            .map(CudaBuffer::num_rows)
2538            .sum();
2539        let delete_rows = deltas
2540            .values()
2541            .filter_map(|delta| delta.delete.as_ref())
2542            .map(CudaBuffer::num_rows)
2543            .sum();
2544        let cache_reused = session_runtime.is_some() || cached_store.is_some();
2545        let mut changed_relation_names = deltas.keys().cloned().collect::<Vec<_>>();
2546        changed_relation_names.sort();
2547
2548        let prior_cached_store = cached_store.take();
2549        let prior_session_runtime = session_runtime.take();
2550
2551        let missing_relation_count = changed_relation_names
2552            .iter()
2553            .filter(|name| !relation_store.contains(name))
2554            .count();
2555        relation_store.try_reserve_relations(missing_relation_count)?;
2556
2557        if deltas.is_empty() {
2558            if let Some(seed) = report_seed {
2559                return Ok(PreparedRelationDeltaCommit {
2560                    provider,
2561                    authoritative_relation_store: relation_store,
2562                    cached_store_slot: cached_store,
2563                    session_runtime_slot: session_runtime,
2564                    staged_base_updates: Vec::new(),
2565                    prospective_cached_store: prior_cached_store,
2566                    prospective_session_runtime: prior_session_runtime,
2567                    report: no_op_delta_report(seed),
2568                });
2569            }
2570        }
2571
2572        let mut working_runtime = match prior_session_runtime {
2573            Some(runtime) => runtime,
2574            None => {
2575                if let Some(materialized_store) = prior_cached_store.as_ref() {
2576                    self.create_session_runtime_from_materialized_store(
2577                        provider.clone(),
2578                        materialized_store,
2579                        false,
2580                    )?
2581                } else {
2582                    self.create_session_runtime(provider.clone(), relation_store, false)?
2583                }
2584            }
2585        };
2586
2587        if prior_cached_store.is_none() {
2588            self.evaluate_with_session_runtime(provider.clone(), &mut working_runtime)?;
2589        }
2590
2591        let delta_stats = working_runtime.executor.apply_deltas_and_recompute(
2592            self.ordinary_plan("session relation-delta recompute")?,
2593            &deltas,
2594        )?;
2595        self.enforce_constraints(&provider, &working_runtime.executor)?;
2596
2597        let mut staged_base_updates = Vec::with_capacity(changed_relation_names.len());
2598        for name in &changed_relation_names {
2599            let updated = working_runtime.executor.store().get(name).ok_or_else(|| {
2600                XlogError::Execution(format!(
2601                    "Delta relation {} missing after runtime recompute",
2602                    name
2603                ))
2604            })?;
2605            let context = format!("cloning staged base relation '{name}'");
2606            staged_base_updates.push((
2607                name.clone(),
2608                provider
2609                    .clone_buffer(updated)
2610                    .map_err(|error| relation_clone_error(context, error))?,
2611            ));
2612        }
2613        let prospective_cached_store = Some(self.bind_materialized_store(
2614            self.clone_prepared_relation_snapshot(&provider, working_runtime.executor.store())?,
2615        ));
2616
2617        let mut report = logic_delta_report(delta_stats, insert_rows, delete_rows);
2618        report.changed_relation_names = changed_relation_names;
2619        report.planner_telemetry =
2620            DeltaPlannerTelemetry::from_delta_report(&report, cache_reused, None);
2621        report.debug_trace = delta_debug_trace(&report);
2622        if let Some(seed) = report_seed {
2623            report.input_delta_count = seed.input_delta_count;
2624            report.changed_relations = seed.changed_relations;
2625            report.coalesced_insert_rows = seed.coalesced_insert_rows;
2626            report.coalesced_delete_rows = seed.coalesced_delete_rows;
2627            report.canceled_rows = seed.canceled_rows;
2628            report.planner_telemetry =
2629                DeltaPlannerTelemetry::from_delta_report(&report, true, None);
2630            report.debug_trace = delta_debug_trace(&report);
2631        }
2632
2633        Ok(PreparedRelationDeltaCommit {
2634            provider,
2635            authoritative_relation_store: relation_store,
2636            cached_store_slot: cached_store,
2637            session_runtime_slot: session_runtime,
2638            staged_base_updates,
2639            prospective_cached_store,
2640            prospective_session_runtime: Some(working_runtime),
2641            report,
2642        })
2643    }
2644
2645    /// Apply an ordered batch of relation deltas after device-side coalescing.
2646    ///
2647    /// A fully canceled batch returns a no-op report without changing the
2648    /// authoritative store or advancing derived runtime state. If preparation
2649    /// fails, the authoritative store remains unchanged. A foreign cache is
2650    /// rejected before device coalescing and is not consumed; after successful
2651    /// identity validation an operational failure may discard the cache.
2652    pub fn apply_relation_delta_batch(
2653        &self,
2654        provider: Arc<CudaKernelProvider>,
2655        relation_store: &mut RelationStore,
2656        cached_store: &mut Option<LogicMaterializedStore>,
2657        delta_batch: Vec<(String, RelationDelta)>,
2658    ) -> Result<LogicDeltaReport> {
2659        self.validate_reusable_state_slots(cached_store.as_ref(), None)?;
2660        self.reject_compiler_generated_query_relation_names(
2661            relation_store.names(),
2662            "persistent caller",
2663        )?;
2664        let prepared_batch =
2665            self.prepare_relation_delta_batch(provider.as_ref(), delta_batch, &BTreeSet::new())?;
2666        let mut session_runtime = None;
2667        let prepared = self.prepare_relation_delta_commit_with_session_runtime(
2668            provider,
2669            relation_store,
2670            cached_store,
2671            &mut session_runtime,
2672            prepared_batch,
2673        )?;
2674        Ok(prepared.commit())
2675    }
2676
2677    /// Apply an ordered batch of relation deltas while preserving session runtime state.
2678    ///
2679    /// A fully canceled batch returns a no-op report without changing the
2680    /// authoritative store or advancing derived runtime state. If preparation
2681    /// fails, the authoritative store remains unchanged while the derived cache
2682    /// and retained runtime slots are left empty. Foreign state is rejected
2683    /// before device coalescing and remains in the caller slots.
2684    pub fn apply_relation_delta_batch_with_session_runtime(
2685        &self,
2686        provider: Arc<CudaKernelProvider>,
2687        relation_store: &mut RelationStore,
2688        cached_store: &mut Option<LogicMaterializedStore>,
2689        session_runtime: &mut Option<LogicSessionRuntime>,
2690        delta_batch: Vec<(String, RelationDelta)>,
2691    ) -> Result<LogicDeltaReport> {
2692        self.validate_reusable_state_slots(cached_store.as_ref(), session_runtime.as_ref())?;
2693        self.reject_compiler_generated_query_relation_names(
2694            relation_store.names(),
2695            "persistent caller",
2696        )?;
2697        let prepared_batch =
2698            self.prepare_relation_delta_batch(provider.as_ref(), delta_batch, &BTreeSet::new())?;
2699        let prepared = self.prepare_relation_delta_commit_with_session_runtime(
2700            provider,
2701            relation_store,
2702            cached_store,
2703            session_runtime,
2704            prepared_batch,
2705        )?;
2706        Ok(prepared.commit())
2707    }
2708
2709    /// Evaluate the program with the given input relations (no profiling).
2710    pub fn evaluate(
2711        &self,
2712        provider: Arc<CudaKernelProvider>,
2713        inputs: HashMap<String, CudaBuffer>,
2714    ) -> Result<LogicEvalResult> {
2715        self.evaluate_with_options(provider, inputs, false)
2716    }
2717
2718    fn finish_nonordinary_resident_selection(
2719        &self,
2720        mut result: LogicEvalResult,
2721        mode: ResidentSelectionMode,
2722    ) -> Result<LogicEvalResult> {
2723        match mode {
2724            ResidentSelectionMode::Disabled => Ok(result),
2725            ResidentSelectionMode::Auto => {
2726                if let Some(stats) = result.stats.as_mut() {
2727                    stats.resident_graph = Some(ResidentGraphExecutionStats::declined(
2728                        ResidentGraphDeclineReason::NonOrdinaryPlan,
2729                    ));
2730                }
2731                Ok(result)
2732            }
2733            ResidentSelectionMode::Require => Err(XlogError::Execution(
2734                "resident conditional-graph execution was required for a non-ordinary program"
2735                    .to_string(),
2736            )),
2737        }
2738    }
2739
2740    fn compiler_generated_query_heads(&self) -> Result<BTreeSet<String>> {
2741        match &self.plan {
2742            LogicExecutionPlan::Ordinary(plan) => {
2743                if plan.generated_query_rules.len() != self.program.queries.len() {
2744                    return Err(XlogError::Execution(format!(
2745                            "compiler-generated query provenance count {} does not match authored query count {}",
2746                            plan.generated_query_rules.len(),
2747                            self.program.queries.len()
2748                        )));
2749                }
2750                let mut heads = BTreeSet::new();
2751                let mut rule_positions = BTreeSet::new();
2752                for (position, provenance) in plan.generated_query_rules.iter().enumerate() {
2753                    if provenance.query_index != position {
2754                        return Err(XlogError::Execution(format!(
2755                                "compiler-generated query provenance position {position} carries query index {}",
2756                                provenance.query_index
2757                            )));
2758                    }
2759                    if !rule_positions.insert((provenance.scc_index, provenance.rule_index)) {
2760                        return Err(XlogError::Execution(format!(
2761                                "compiler-generated query provenance {} reuses compiled rule scc={} rule={}",
2762                                provenance.query_index, provenance.scc_index, provenance.rule_index
2763                            )));
2764                    }
2765                    let expected_head = format!("__xlog_query_{}", provenance.query_index);
2766                    let rule = plan
2767                            .rules_by_scc
2768                            .get(provenance.scc_index)
2769                            .and_then(|rules| rules.get(provenance.rule_index))
2770                            .ok_or_else(|| {
2771                                XlogError::Execution(format!(
2772                                    "compiler-generated query provenance {} references missing compiled rule scc={} rule={}",
2773                                    provenance.query_index,
2774                                    provenance.scc_index,
2775                                    provenance.rule_index
2776                                ))
2777                            })?;
2778                    if rule.head != expected_head {
2779                        return Err(XlogError::Execution(format!(
2780                                "compiler-generated query provenance {} expects head {expected_head} but references authored head {}",
2781                                provenance.query_index, rule.head
2782                            )));
2783                    }
2784                    let occurrence_count = plan
2785                        .rules_by_scc
2786                        .iter()
2787                        .flatten()
2788                        .filter(|candidate| candidate.head == expected_head)
2789                        .count();
2790                    if occurrence_count != 1 {
2791                        return Err(XlogError::Execution(format!(
2792                                "compiler-generated query head {expected_head} must have exactly one compiled rule, found {occurrence_count}"
2793                            )));
2794                    }
2795                    heads.insert(expected_head);
2796                }
2797                Ok(heads)
2798            }
2799            _ => Ok((0..self.program.queries.len())
2800                .map(|index| format!("__xlog_query_{index}"))
2801                .collect()),
2802        }
2803    }
2804
2805    fn reject_compiler_generated_query_relation_names<'a>(
2806        &self,
2807        names: impl IntoIterator<Item = &'a str>,
2808        relation_source: &str,
2809    ) -> Result<()> {
2810        let generated_query_heads = self.compiler_generated_query_heads()?;
2811        if let Some(name) = names
2812            .into_iter()
2813            .find(|name| generated_query_heads.contains(*name))
2814        {
2815            return Err(XlogError::Execution(format!(
2816                "{relation_source} relation {name} collides with generated query head"
2817            )));
2818        }
2819        Ok(())
2820    }
2821
2822    fn evaluate_ordinary_with_resident_mode(
2823        &self,
2824        provider: Arc<CudaKernelProvider>,
2825        inputs: HashMap<String, CudaBuffer>,
2826        profiling: bool,
2827        mode: ResidentSelectionMode,
2828    ) -> Result<LogicEvalResult> {
2829        let mut latency_diagnostic =
2830            resident_latency_diagnostics_enabled()?.then(ResidentLatencyDiagnostic::new);
2831        let total_started = latency_diagnostic
2832            .as_ref()
2833            .map(|_| std::time::Instant::now());
2834        let certificate_input_started = latency_diagnostic
2835            .as_ref()
2836            .map(|_| std::time::Instant::now());
2837        let ordinary_plan = self.ordinary_plan("resident route certification")?;
2838        if self.program.queries.is_empty() {
2839            return self.evaluate_existing_gpu_after_resident_decline(
2840                provider,
2841                inputs,
2842                profiling,
2843                ordinary_plan,
2844                mode,
2845                ResidentGraphDeclineReason::FullStoreRequested,
2846            );
2847        }
2848        let certificate_initialization_started = latency_diagnostic
2849            .as_ref()
2850            .map(|_| std::time::Instant::now());
2851        let certification = if latency_diagnostic.is_some() {
2852            self.resident_certified_plan_with_outcome_for_plan(ordinary_plan)
2853        } else {
2854            self.resident_certified_plan_for_plan(ordinary_plan)
2855                .map(|certified| (certified, false, false))
2856        };
2857        let (certified_plan, certificate_cache_was_warm, certificate_initialized_here) =
2858            match certification {
2859                Ok(outcome) => outcome,
2860                Err(error) => {
2861                    return self.evaluate_existing_gpu_after_resident_decline(
2862                        provider,
2863                        inputs,
2864                        profiling,
2865                        ordinary_plan,
2866                        mode,
2867                        ResidentGraphDeclineReason::WorkspaceUnbounded {
2868                            detail: error.to_string(),
2869                        },
2870                    )
2871                }
2872            };
2873        let certificate = certified_plan.certificate();
2874        if !certificate.is_supported() {
2875            let reason = certificate.declines().first().cloned().unwrap_or_else(|| {
2876                ResidentGraphDeclineReason::WorkspaceUnbounded {
2877                    detail: "route inspection did not produce a resident certificate".into(),
2878                }
2879            });
2880            return self.evaluate_existing_gpu_after_resident_decline(
2881                provider,
2882                inputs,
2883                profiling,
2884                ordinary_plan,
2885                mode,
2886                reason,
2887            );
2888        }
2889        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2890            let elapsed_ns = resident_latency_elapsed_ns(certificate_initialization_started);
2891            diagnostic.certificate_cache_was_warm = certificate_cache_was_warm;
2892            diagnostic.certificate_initialized_here = certificate_initialized_here;
2893            if certificate_initialized_here {
2894                diagnostic.certificate_initialization_ns = elapsed_ns;
2895            } else {
2896                diagnostic.certificate_cache_access_ns = elapsed_ns;
2897            }
2898        }
2899        let input_setup_started = latency_diagnostic
2900            .as_ref()
2901            .map(|_| std::time::Instant::now());
2902
2903        for (name, buffer) in &inputs {
2904            let expected_schema = self.schemas.get(name).ok_or_else(|| {
2905                XlogError::Execution(format!(
2906                    "Input relation {name} not declared in program schemas"
2907                ))
2908            })?;
2909            ensure_schema_type_compatible(expected_schema, buffer.schema()).map_err(|error| {
2910                XlogError::Execution(format!("Input relation {name} schema mismatch: {error}"))
2911            })?;
2912        }
2913
2914        if let Some(relation) = inputs.iter().find_map(|(name, buffer)| {
2915            (!Self::resident_input_is_local(&provider, buffer)).then(|| name.clone())
2916        }) {
2917            return self.evaluate_existing_gpu_after_resident_decline(
2918                provider,
2919                inputs,
2920                profiling,
2921                ordinary_plan,
2922                mode,
2923                ResidentGraphDeclineReason::ImportedInputUnsupported { relation },
2924            );
2925        }
2926
2927        for (name, buffer) in &inputs {
2928            if !buffer.canonical_full_row_set_certified() {
2929                provider
2930                    .validated_logical_row_count(buffer)
2931                    .map_err(|error| {
2932                        XlogError::Execution(format!(
2933                            "Input relation {name} has invalid logical row metadata: {error}"
2934                        ))
2935                    })?;
2936            }
2937        }
2938
2939        let runtime = Arc::clone(provider.memory().runtime().ok_or_else(|| {
2940            XlogError::Execution(
2941                "internal invariant violated: CUDA provider has no owned runtime".into(),
2942            )
2943        })?);
2944        if !Arc::ptr_eq(provider.device(), provider.memory().device())
2945            || !Arc::ptr_eq(provider.device(), runtime.device())
2946            || !runtime.supports_block_use_tracking()
2947        {
2948            return Err(XlogError::Execution(
2949                "internal invariant violated: CUDA provider runtime ownership graph is inconsistent"
2950                    .into(),
2951            ));
2952        }
2953        let resident_provider = provider;
2954        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2955            diagnostic.runtime_bytes[0] = runtime.bytes_outstanding();
2956            diagnostic.manager_bytes[0] = resident_provider.memory().allocated_bytes();
2957        }
2958
2959        let resident_inputs = inputs;
2960
2961        let mut canonical_replacements = HashMap::new();
2962        let canonicalization = resident_inputs.iter().try_for_each(|(name, buffer)| {
2963            let expected_schema = self.schemas.get(name).ok_or_else(|| {
2964                XlogError::Execution(format!(
2965                    "Input relation {name} not declared in program schemas"
2966                ))
2967            })?;
2968            ensure_schema_type_compatible(expected_schema, buffer.schema()).map_err(|error| {
2969                XlogError::Execution(format!("Input relation {name} schema mismatch: {error}"))
2970            })?;
2971            if buffer.schema() == expected_schema && buffer.canonical_full_row_set_certified() {
2972                return Ok(());
2973            }
2974            let mut normalized = None;
2975            let canonical_source = if buffer.schema() == expected_schema {
2976                buffer
2977            } else {
2978                let mut clone = resident_provider.clone_buffer(buffer)?;
2979                clone.set_schema(expected_schema.clone());
2980                normalized.insert(clone)
2981            };
2982            let canonical = resident_provider.union_many_gpu(&[canonical_source])?;
2983            if !canonical.canonical_full_row_set_certified() {
2984                return Err(XlogError::Execution(format!(
2985                    "resident input {name} did not acquire a full-row set proof"
2986                )));
2987            }
2988            canonical_replacements.insert(name.clone(), canonical);
2989            Ok(())
2990        });
2991        if let Err(error) = canonicalization {
2992            drop(canonical_replacements);
2993            resident_provider.device().synchronize().map_err(|cleanup| {
2994                    XlogError::Kernel(format!(
2995                        "resident input canonicalization failed ({error}); cleanup synchronization failed: {cleanup}"
2996                    ))
2997                })?;
2998            runtime.reap_pending().map_err(|cleanup| {
2999                    XlogError::Kernel(format!(
3000                        "resident input canonicalization failed ({error}); cleanup reap failed: {cleanup}"
3001                    ))
3002                })?;
3003            return self.evaluate_existing_gpu_after_resident_decline(
3004                resident_provider,
3005                resident_inputs,
3006                profiling,
3007                ordinary_plan,
3008                mode,
3009                ResidentGraphDeclineReason::WorkspaceUnbounded {
3010                    detail: format!("input full-row canonicalization failed: {error}"),
3011                },
3012            );
3013        }
3014        let resident_inputs = resident_inputs
3015            .into_iter()
3016            .map(|(name, buffer)| {
3017                let canonical = canonical_replacements.remove(&name).unwrap_or(buffer);
3018                (name, canonical)
3019            })
3020            .collect::<HashMap<_, _>>();
3021        let mut input_shape = resident_inputs
3022            .iter()
3023            .map(|(name, buffer)| (name.clone(), buffer.schema().clone(), buffer.num_rows()))
3024            .collect::<Vec<_>>();
3025        input_shape.sort_by(|left, right| left.0.cmp(&right.0));
3026        // Fixed authored facts need no replacement when there are no external
3027        // inputs. With external inputs, fact merging can change captured source
3028        // geometry, so reuse is limited to programs without authored facts.
3029        let reusable_source_shape = input_shape.is_empty() || self.program.facts().next().is_none();
3030
3031        if let Some(diagnostic) = latency_diagnostic.as_mut() {
3032            diagnostic.input_setup_ns = resident_latency_elapsed_ns(input_setup_started);
3033            diagnostic.certificate_input_ns =
3034                resident_latency_elapsed_ns(certificate_input_started);
3035            diagnostic.runtime_bytes[1] = runtime.bytes_outstanding();
3036            diagnostic.manager_bytes[1] = resident_provider.memory().allocated_bytes();
3037        }
3038        let prepare_started = latency_diagnostic
3039            .as_ref()
3040            .map(|_| std::time::Instant::now());
3041        let prepare_options = latency_diagnostic
3042            .as_ref()
3043            .map(|diagnostic| {
3044                ResidentGraphPrepareOptions::default()
3045                    .with_latency_diagnostic_sample(diagnostic.sample)
3046            })
3047            .unwrap_or_default();
3048        let cached = if reusable_source_shape {
3049            self.reusable_state_identity
3050                .take_resident_evaluation_owner(&resident_provider, &input_shape, profiling)?
3051        } else {
3052            None
3053        };
3054        let (mut executor, mut prepared) = if let Some(mut owner) = cached {
3055            for (name, input) in resident_inputs {
3056                let target = owner.executor.store_mut().get_mut(&name).ok_or_else(|| {
3057                    XlogError::Execution(format!(
3058                        "resident source {name} disappeared before in-place replacement"
3059                    ))
3060                })?;
3061                resident_provider.overwrite_resident_source(target, &input)?;
3062            }
3063            owner
3064                .prepared
3065                .refresh_source_set_snapshots(&owner.executor)
3066                .map_err(Self::resident_execution_error)?;
3067            (owner.executor, owner.prepared)
3068        } else {
3069            let mut executor = self.prepare_resident_executor(
3070                &resident_provider,
3071                resident_inputs,
3072                profiling,
3073                ordinary_plan,
3074            )?;
3075            let prepared = match executor
3076                .prepare_certified_resident_graph(certified_plan.as_ref(), prepare_options)
3077            {
3078                Ok(prepared) => prepared,
3079                Err(ResidentGraphExecutionError::Declined(reason)) => {
3080                    runtime
3081                        .reap_pending()
3082                        .map_err(|error| XlogError::Kernel(error.to_string()))?;
3083                    return match mode {
3084                        ResidentSelectionMode::Auto => {
3085                            executor.execute_plan(ordinary_plan)?;
3086                            let mut result = self.finish_ordinary_evaluation(
3087                                &resident_provider,
3088                                executor,
3089                                profiling,
3090                                None,
3091                                None,
3092                            )?;
3093                            if let Some(stats) = result.stats.as_mut() {
3094                                stats.resident_graph =
3095                                    Some(ResidentGraphExecutionStats::declined(reason));
3096                            }
3097                            Ok(result)
3098                        }
3099                        ResidentSelectionMode::Require => Err(XlogError::Execution(format!(
3100                            "resident conditional-graph execution was required but declined: {reason:?}"
3101                        ))),
3102                        ResidentSelectionMode::Disabled => unreachable!(
3103                            "disabled resident selection does not call the resident evaluator"
3104                        ),
3105                    };
3106                }
3107                Err(error) => return Err(Self::resident_execution_error(error)),
3108            };
3109            (executor, prepared)
3110        };
3111        if let Some(diagnostic) = latency_diagnostic.as_mut() {
3112            diagnostic.prepare_capture_allocation_ns = resident_latency_elapsed_ns(prepare_started);
3113            diagnostic.runtime_bytes[2] = runtime.bytes_outstanding();
3114            diagnostic.manager_bytes[2] = resident_provider.memory().allocated_bytes();
3115        }
3116        let prepare_diagnostic = prepared.take_prepare_diagnostic();
3117
3118        let transfer_scope = resident_provider.begin_resident_host_transfer_scope();
3119
3120        let launch_started = latency_diagnostic
3121            .as_ref()
3122            .map(|_| std::time::Instant::now());
3123        let in_flight = prepared
3124            .launch(&executor)
3125            .map_err(Self::resident_execution_error)?;
3126        if let Some(diagnostic) = latency_diagnostic.as_mut() {
3127            diagnostic.launch_submission_ns = resident_latency_elapsed_ns(launch_started);
3128            diagnostic.runtime_bytes[3] = runtime.bytes_outstanding();
3129            diagnostic.manager_bytes[3] = resident_provider.memory().allocated_bytes();
3130        }
3131        let sync_started = latency_diagnostic
3132            .as_ref()
3133            .map(|_| std::time::Instant::now());
3134        let synchronized = in_flight
3135            .synchronize_core()
3136            .map_err(Self::resident_execution_error)?;
3137        if let Some(diagnostic) = latency_diagnostic.as_mut() {
3138            diagnostic.sync_wall_ns = resident_latency_elapsed_ns(sync_started);
3139            diagnostic.runtime_bytes[4] = runtime.bytes_outstanding();
3140            diagnostic.manager_bytes[4] = resident_provider.memory().allocated_bytes();
3141        }
3142
3143        let core_snapshot = transfer_scope.snapshot();
3144        let core_transfers = ResidentGraphCoreTransferStats {
3145            tracked_htod_calls: core_snapshot.tracked_htod_calls,
3146            tracked_htod_bytes: core_snapshot.tracked_htod_bytes,
3147            tracked_dtoh_calls: core_snapshot.tracked_dtoh_calls,
3148            tracked_dtoh_bytes: core_snapshot.tracked_dtoh_bytes,
3149            provider_dtoh_calls: core_snapshot.provider_dtoh_calls,
3150            untracked_metadata_dtoh_calls: core_snapshot.untracked_metadata_dtoh_calls,
3151        };
3152        if core_transfers.tracked_htod_calls != 0
3153            || core_transfers.tracked_htod_bytes != 0
3154            || core_transfers.tracked_dtoh_calls != 0
3155            || core_transfers.tracked_dtoh_bytes != 0
3156            || core_transfers.provider_dtoh_calls != 0
3157            || core_transfers.untracked_metadata_dtoh_calls != 0
3158            || core_snapshot.final_dtoh_calls != 0
3159            || core_snapshot.final_dtoh_bytes != 0
3160            || core_snapshot.final_pinned_receipts != 0
3161        {
3162            return Err(XlogError::Execution(
3163                "resident conditional-graph core performed a host transfer".into(),
3164            ));
3165        }
3166        // The successful launch and one successful completion wait above are
3167        // owned by this invocation. Provider-wide telemetry also includes
3168        // concurrent callers and cannot establish these per-call counts.
3169        let graph_launches = 1;
3170        let terminal_synchronizations = 1;
3171        let host_iterations = 0;
3172        let host_allocations = 0;
3173        let host_status_injections = 0;
3174        let deterministic_d2h_violations = core_snapshot.deterministic_dtoh_violations;
3175        if deterministic_d2h_violations != 0 {
3176            return Err(XlogError::Execution(format!(
3177                "resident conditional-graph core attempted {deterministic_d2h_violations} forbidden device-to-host transfers"
3178            )));
3179        }
3180
3181        let observation_started = latency_diagnostic
3182            .as_ref()
3183            .map(|_| std::time::Instant::now());
3184        let observed = synchronized
3185            .observe_final_receipt()
3186            .map_err(Self::resident_execution_error)?;
3187        if let Some(diagnostic) = latency_diagnostic.as_mut() {
3188            let observation_ns = resident_latency_elapsed_ns(observation_started);
3189            let phase = observed.phase_timings().ok_or_else(|| {
3190                XlogError::Execution(
3191                    "resident latency diagnostics missing final-observation timings".into(),
3192                )
3193            })?;
3194            diagnostic.receipt_d2h_ns = phase.receipt_d2h_ns;
3195            diagnostic.receipt_decode_schema_staging_ns = phase.decode_schema_staging_ns;
3196            diagnostic.owner_teardown_residual_ns = observation_ns
3197                .saturating_sub(phase.receipt_d2h_ns)
3198                .saturating_sub(phase.decode_schema_staging_ns);
3199            diagnostic.staged_outputs = observed.staged_output_count();
3200            diagnostic.relation_registrations = observed.relation_registration_count();
3201            diagnostic.runtime_bytes[5] = runtime.bytes_outstanding();
3202            diagnostic.manager_bytes[5] = resident_provider.memory().allocated_bytes();
3203        }
3204        let encoded_len = u64::try_from(observed.encoded_len())
3205            .map_err(|_| XlogError::Execution("resident receipt byte length exceeds u64".into()))?;
3206        let device_elapsed_ns = observed.device_elapsed_ns();
3207        let device_scan_invocations = observed.device_scan_invocations();
3208        let device_filter_invocations = observed.device_filter_invocations();
3209        let semantic_scan_invocations = observed.semantic_scan_invocations();
3210        let semantic_filter_invocations = observed.semantic_filter_invocations();
3211        let staged_store_mutations = observed.staged_output_count();
3212        let iterations = observed.iterations();
3213        let final_after = transfer_scope.snapshot();
3214        let final_observation = ResidentGraphFinalObservationStats {
3215            dtoh_calls: final_after.final_dtoh_calls,
3216            dtoh_bytes: final_after.final_dtoh_bytes,
3217            pinned_receipts: final_after.final_pinned_receipts,
3218        };
3219        if final_observation.dtoh_calls != 1
3220            || final_observation.dtoh_bytes != encoded_len
3221            || final_observation.pinned_receipts != 1
3222        {
3223            return Err(XlogError::Execution(format!(
3224                    "resident final observation invariant failed: calls={}, bytes={}, pinned={} expected_bytes={encoded_len}",
3225                    final_observation.dtoh_calls,
3226                    final_observation.dtoh_bytes,
3227                    final_observation.pinned_receipts,
3228                )));
3229        }
3230        if let Some(diagnostic) = latency_diagnostic.as_mut() {
3231            diagnostic.device_event_ns = device_elapsed_ns;
3232        }
3233        let commit_started = latency_diagnostic
3234            .as_ref()
3235            .map(|_| std::time::Instant::now());
3236        let prepared = if reusable_source_shape {
3237            Some(
3238                observed
3239                    .commit_for_reuse(&mut executor)
3240                    .map_err(Self::resident_execution_error)?,
3241            )
3242        } else {
3243            observed
3244                .commit(&mut executor)
3245                .map_err(Self::resident_execution_error)?;
3246            None
3247        };
3248        if let Some(diagnostic) = latency_diagnostic.as_mut() {
3249            diagnostic.commit_ns = resident_latency_elapsed_ns(commit_started);
3250            diagnostic.runtime_bytes[6] = runtime.bytes_outstanding();
3251            diagnostic.manager_bytes[6] = resident_provider.memory().allocated_bytes();
3252        }
3253
3254        let telemetry_started = latency_diagnostic
3255            .as_ref()
3256            .map(|_| std::time::Instant::now());
3257        let timed_scan_filter_invocations = device_scan_invocations
3258            .checked_add(device_filter_invocations)
3259            .ok_or_else(|| {
3260                XlogError::Execution("resident device invocation count overflow".into())
3261            })?;
3262        let telemetry = ResidentGraphExecutionStats {
3263            selection: ResidentGraphSelectionKind::ResidentConditionalGraph,
3264            decline: None,
3265            conditional_graph_launches: graph_launches,
3266            terminal_synchronizations,
3267            host_iterations,
3268            host_allocations,
3269            host_status_injections,
3270            deterministic_d2h_violations,
3271            host_dispatched_scan_ops: 0,
3272            host_dispatched_filter_ops: 0,
3273            device_scan_invocations,
3274            device_filter_invocations,
3275            semantic_scan_invocations,
3276            semantic_filter_invocations,
3277            staged_store_mutations,
3278            deferred_profile: ResidentGraphDeferredProfile {
3279                timed_scan_filter_invocations,
3280                device_elapsed_ns,
3281                final_sync_misattributed_ns: 0,
3282            },
3283            core_transfers,
3284            final_observation,
3285        };
3286        if let Some(diagnostic) = latency_diagnostic.as_mut() {
3287            diagnostic.result_stats_construction_ns =
3288                resident_latency_elapsed_ns(telemetry_started);
3289        }
3290        let completed_profile = Some(ResidentCompletedProfile {
3291            telemetry,
3292            iterations,
3293        });
3294        let result = if let Some(mut prepared) = prepared {
3295            let result = self.collect_ordinary_evaluation(
3296                &resident_provider,
3297                &mut executor,
3298                profiling,
3299                completed_profile,
3300                true,
3301                latency_diagnostic.as_mut(),
3302            )?;
3303            prepared
3304                .restore_outputs(&mut executor)
3305                .map_err(Self::resident_execution_error)?;
3306            self.reusable_state_identity
3307                .retain_resident_evaluation_owner(ResidentEvaluationOwner {
3308                    provider: Arc::clone(&resident_provider),
3309                    input_shape,
3310                    profiling,
3311                    executor,
3312                    prepared,
3313                })?;
3314            result
3315        } else {
3316            self.finish_ordinary_evaluation(
3317                &resident_provider,
3318                executor,
3319                profiling,
3320                completed_profile,
3321                latency_diagnostic.as_mut(),
3322            )?
3323        };
3324        if let Some(diagnostic) = latency_diagnostic.as_mut() {
3325            diagnostic.runtime_bytes[7] = runtime.bytes_outstanding();
3326            diagnostic.manager_bytes[7] = resident_provider.memory().allocated_bytes();
3327        }
3328        let diagnostic_lines = if latency_diagnostic.is_some() {
3329            let total_ns = resident_latency_elapsed_ns(total_started);
3330            finalized_resident_latency_diagnostic_lines(
3331                total_ns,
3332                latency_diagnostic.as_ref(),
3333                prepare_diagnostic
3334                    .map(|diagnostic| move || diagnostic.into_snapshot().format_line()),
3335                ResidentLatencyDiagnostic::format_line,
3336            )
3337        } else {
3338            None
3339        };
3340        if let Some(diagnostic_lines) = diagnostic_lines {
3341            for line in diagnostic_lines.into_iter().flatten() {
3342                eprintln!("{line}");
3343            }
3344        }
3345        Ok(result)
3346    }
3347
3348    fn evaluate_existing_gpu_after_resident_decline(
3349        &self,
3350        provider: Arc<CudaKernelProvider>,
3351        inputs: HashMap<String, CudaBuffer>,
3352        profiling: bool,
3353        plan: &ExecutionPlan,
3354        mode: ResidentSelectionMode,
3355        reason: ResidentGraphDeclineReason,
3356    ) -> Result<LogicEvalResult> {
3357        match mode {
3358            ResidentSelectionMode::Require => Err(XlogError::Execution(format!(
3359                "resident conditional-graph execution was required but declined: {reason:?}"
3360            ))),
3361            ResidentSelectionMode::Auto | ResidentSelectionMode::Disabled => {
3362                let mut executor = self.prepare_executor(&provider, inputs, profiling)?;
3363                executor.execute_plan(plan)?;
3364                let mut result =
3365                    self.finish_ordinary_evaluation(&provider, executor, profiling, None, None)?;
3366                if let Some(stats) = result.stats.as_mut() {
3367                    stats.resident_graph = Some(ResidentGraphExecutionStats::declined(reason));
3368                }
3369                Ok(result)
3370            }
3371        }
3372    }
3373
3374    fn resident_input_is_local(provider: &CudaKernelProvider, buffer: &CudaBuffer) -> bool {
3375        let expected_manager = Arc::as_ptr(provider.memory()) as usize;
3376        buffer.num_rows_device().memory_manager_ptr_value() == expected_manager
3377            && buffer.columns().iter().all(|column| {
3378                matches!(
3379                    column,
3380                    CudaColumn::Owned(slice)
3381                        if slice.memory_manager_ptr_value() == expected_manager
3382                )
3383            })
3384    }
3385
3386    fn resident_execution_error(error: ResidentGraphExecutionError) -> XlogError {
3387        XlogError::Execution(error.to_string())
3388    }
3389
3390    fn finish_ordinary_evaluation(
3391        &self,
3392        provider: &Arc<CudaKernelProvider>,
3393        mut executor: Executor,
3394        profiling: bool,
3395        resident_profile: Option<ResidentCompletedProfile>,
3396        mut latency_diagnostic: Option<&mut ResidentLatencyDiagnostic>,
3397    ) -> Result<LogicEvalResult> {
3398        let result = self.collect_ordinary_evaluation(
3399            provider,
3400            &mut executor,
3401            profiling,
3402            resident_profile,
3403            false,
3404            latency_diagnostic.as_deref_mut(),
3405        )?;
3406        let executor_drop_started = latency_diagnostic
3407            .as_ref()
3408            .map(|_| std::time::Instant::now());
3409        drop(executor);
3410        if let Some(diagnostic) = latency_diagnostic.as_mut() {
3411            diagnostic.executor_store_teardown_ns =
3412                resident_latency_elapsed_ns(executor_drop_started);
3413        }
3414        Ok(result)
3415    }
3416
3417    fn collect_ordinary_evaluation(
3418        &self,
3419        provider: &Arc<CudaKernelProvider>,
3420        executor: &mut Executor,
3421        profiling: bool,
3422        resident_profile: Option<ResidentCompletedProfile>,
3423        retain_query_owners: bool,
3424        mut latency_diagnostic: Option<&mut ResidentLatencyDiagnostic>,
3425    ) -> Result<LogicEvalResult> {
3426        let result_started = latency_diagnostic
3427            .as_ref()
3428            .map(|_| std::time::Instant::now());
3429        self.enforce_constraints(provider, executor)?;
3430
3431        let mut queries = Vec::with_capacity(self.program.queries.len());
3432        for (index, query) in self.program.queries.iter().enumerate() {
3433            let internal_relation_name = format!("__xlog_query_{index}");
3434            let missing_query = || {
3435                XlogError::Execution(format!(
3436                    "Missing query result relation {internal_relation_name} (compiler bug?)"
3437                ))
3438            };
3439            let buffer = if retain_query_owners {
3440                provider.clone_buffer(
3441                    executor
3442                        .store()
3443                        .get(&internal_relation_name)
3444                        .ok_or_else(missing_query)?,
3445                )?
3446            } else {
3447                executor
3448                    .store_mut()
3449                    .remove(&internal_relation_name)
3450                    .ok_or_else(missing_query)?
3451            };
3452            queries.push(self.logic_query_result(
3453                provider.as_ref(),
3454                index,
3455                query,
3456                internal_relation_name,
3457                buffer,
3458            )?);
3459        }
3460
3461        let mut stats = if profiling {
3462            let total_output_rows =
3463                total_logical_rows(provider.as_ref(), queries.iter().map(|query| &query.buffer))?;
3464            Some(executor.execution_stats(total_output_rows))
3465        } else {
3466            None
3467        };
3468        if let (Some(stats), Some(profile)) = (stats.as_mut(), resident_profile) {
3469            let scan_count =
3470                usize::try_from(profile.telemetry.device_scan_invocations).map_err(|_| {
3471                    XlogError::Execution("resident scan profile count exceeds usize".into())
3472                })?;
3473            let filter_count = usize::try_from(profile.telemetry.device_filter_invocations)
3474                .map_err(|_| {
3475                    XlogError::Execution("resident filter profile count exceeds usize".into())
3476                })?;
3477            let (num_rules, is_recursive) = match &self.plan {
3478                LogicExecutionPlan::Ordinary(plan) => (
3479                    plan.rules_by_scc.iter().map(Vec::len).sum(),
3480                    plan.sccs.iter().any(|scc| scc.is_recursive),
3481                ),
3482                _ => (0, false),
3483            };
3484            let mut stratum = StratumStats::new(0, num_rules, is_recursive);
3485            stratum.iterations = profile.iterations as usize;
3486            stratum.duration_us = profile.telemetry.deferred_profile.device_elapsed_ns / 1_000;
3487            stratum.ops.reserve(scan_count.saturating_add(filter_count));
3488            stratum.ops.extend((0..scan_count).map(|_| OpStats {
3489                op_name: "scan".to_string(),
3490                ..OpStats::default()
3491            }));
3492            stratum.ops.extend((0..filter_count).map(|_| OpStats {
3493                op_name: "filter".to_string(),
3494                ..OpStats::default()
3495            }));
3496            stats.total_duration_us = stratum.duration_us;
3497            stats.strata = vec![stratum];
3498            stats.resident_graph = Some(profile.telemetry);
3499        }
3500
3501        let result = LogicEvalResult { queries, stats };
3502        if let Some(diagnostic) = latency_diagnostic.as_mut() {
3503            diagnostic.result_stats_construction_ns = diagnostic
3504                .result_stats_construction_ns
3505                .saturating_add(resident_latency_elapsed_ns(result_started));
3506            diagnostic.remaining_store_relations_before_drop = executor.store().len();
3507        }
3508        Ok(result)
3509    }
3510
3511    fn prepare_resident_executor(
3512        &self,
3513        provider: &Arc<CudaKernelProvider>,
3514        inputs: HashMap<String, CudaBuffer>,
3515        profiling: bool,
3516        plan: &ExecutionPlan,
3517    ) -> Result<Executor> {
3518        let derived_relations = plan
3519            .rules_by_scc
3520            .iter()
3521            .flatten()
3522            .map(|rule| rule.head.clone())
3523            .collect::<BTreeSet<_>>();
3524        self.prepare_executor_excluding_derived_placeholders(
3525            provider,
3526            inputs,
3527            profiling,
3528            Some(&derived_relations),
3529        )
3530    }
3531
3532    fn prepare_executor_excluding_derived_placeholders(
3533        &self,
3534        provider: &Arc<CudaKernelProvider>,
3535        inputs: HashMap<String, CudaBuffer>,
3536        profiling: bool,
3537        derived_relations: Option<&BTreeSet<String>>,
3538    ) -> Result<Executor> {
3539        let mut executor = Executor::new(provider.clone());
3540        executor.set_profiling(profiling);
3541        for (name, rel_id) in &self.rel_ids {
3542            executor.register_relation(*rel_id, name);
3543        }
3544
3545        let arity_qualified_predicates = self.arity_qualified_fact_predicates();
3546        let fact_rows = self.fact_rows_by_relation(arity_qualified_predicates);
3547        // Multi-row fact-only sources must be sorted and certified before
3548        // resident capture. Caller inputs still need a device union.
3549        let directly_loaded_fact_names = fact_rows
3550            .iter()
3551            .filter(|(name, rows)| {
3552                !inputs.contains_key(*name)
3553                    && (rows.len() == 1
3554                        || self
3555                            .schemas
3556                            .get(*name)
3557                            .is_some_and(Self::host_fact_rows_can_be_canonicalized))
3558            })
3559            .map(|(name, _)| name.clone())
3560            .collect::<BTreeSet<_>>();
3561
3562        let mut empty_names = Vec::new();
3563        let mut empty_schemas = Vec::new();
3564        for (name, schema) in &self.schemas {
3565            let is_derived_placeholder = derived_relations.is_some_and(|set| set.contains(name))
3566                && !fact_rows.contains_key(name);
3567            if is_derived_placeholder
3568                || inputs.contains_key(name)
3569                || directly_loaded_fact_names.contains(name)
3570            {
3571                continue;
3572            }
3573            empty_names.push(name);
3574            empty_schemas.push(schema.clone());
3575        }
3576        for (name, buffer) in empty_names
3577            .into_iter()
3578            .zip(provider.create_empty_buffers(&empty_schemas)?)
3579        {
3580            executor.store_mut().put(name, buffer);
3581        }
3582
3583        for (name, buffer) in inputs {
3584            let schema = self.schemas.get(&name).ok_or_else(|| {
3585                XlogError::Execution(format!(
3586                    "Input relation {} not declared in program schemas",
3587                    name
3588                ))
3589            })?;
3590            ensure_schema_type_compatible(schema, buffer.schema()).map_err(|e| {
3591                XlogError::Execution(format!("Input relation {} schema mismatch: {}", name, e))
3592            })?;
3593            executor.store_mut().put(&name, buffer);
3594        }
3595
3596        self.load_grouped_facts_into_store(
3597            provider,
3598            executor.store_mut(),
3599            fact_rows,
3600            &directly_loaded_fact_names,
3601        )?;
3602        Ok(executor)
3603    }
3604
3605    fn executor_from_materialized_store(
3606        &self,
3607        provider: Arc<CudaKernelProvider>,
3608        relation_store: &RelationStore,
3609        profiling: bool,
3610    ) -> Result<Executor> {
3611        let mut executor = Executor::new(provider.clone());
3612        executor.set_profiling(profiling);
3613        for (name, rel_id) in &self.rel_ids {
3614            executor.register_relation(*rel_id, name);
3615        }
3616
3617        for (name, schema) in &self.schemas {
3618            executor
3619                .store_mut()
3620                .put(name, provider.create_empty_buffer(schema.clone())?);
3621        }
3622
3623        for name in relation_store.names() {
3624            let buffer = relation_store.get(name).ok_or_else(|| {
3625                XlogError::Execution(format!(
3626                    "Persistent relation {} disappeared during evaluation",
3627                    name
3628                ))
3629            })?;
3630            let schema = self.schemas.get(name).ok_or_else(|| {
3631                XlogError::Execution(format!(
3632                    "Persistent relation {} not declared in program schemas",
3633                    name
3634                ))
3635            })?;
3636            ensure_schema_type_compatible(schema, buffer.schema()).map_err(|e| {
3637                XlogError::Execution(format!(
3638                    "Persistent relation {} schema mismatch: {}",
3639                    name, e
3640                ))
3641            })?;
3642            executor
3643                .store_mut()
3644                .put(name, provider.clone_buffer(buffer)?);
3645        }
3646
3647        Ok(executor)
3648    }
3649
3650    #[cfg(test)]
3651    fn resident_certified_plan(&self) -> Result<Arc<ResidentGraphCertifiedPlan>> {
3652        let plan = self.ordinary_plan("resident route certification")?;
3653        self.resident_certified_plan_for_plan(plan)
3654    }
3655
3656    #[cfg(test)]
3657    fn resident_certified_plan_with_outcome(
3658        &self,
3659    ) -> Result<(Arc<ResidentGraphCertifiedPlan>, bool, bool)> {
3660        let plan = self.ordinary_plan("resident route certification")?;
3661        self.resident_certified_plan_with_outcome_for_plan(plan)
3662    }
3663
3664    fn resident_certified_plan_with_outcome_for_plan(
3665        &self,
3666        plan: &ExecutionPlan,
3667    ) -> Result<(Arc<ResidentGraphCertifiedPlan>, bool, bool)> {
3668        self.reusable_state_identity
3669            .get_or_init_resident_certification_with_outcome(|| self.inspect_resident_plan(plan))
3670    }
3671
3672    fn resident_certified_plan_for_plan(
3673        &self,
3674        plan: &ExecutionPlan,
3675    ) -> Result<Arc<ResidentGraphCertifiedPlan>> {
3676        self.reusable_state_identity
3677            .get_or_init_resident_certification(|| self.inspect_resident_plan(plan))
3678    }
3679
3680    fn inspect_resident_plan(&self, plan: &ExecutionPlan) -> Result<ResidentGraphCertifiedPlan> {
3681        let resident_plan = self.resident_dependency_closed_plan(plan);
3682        let catalog = ResidentGraphSchemaCatalog::from_named_schemas(
3683            self.rel_ids.iter().filter_map(|(name, relation)| {
3684                self.schemas
3685                    .get(name)
3686                    .cloned()
3687                    .map(|schema| (name.clone(), *relation, schema))
3688            }),
3689        );
3690        ResidentGraphCertifiedPlan::inspect(Arc::new(resident_plan), &catalog)
3691    }
3692
3693    fn resident_dependency_closed_plan(&self, plan: &ExecutionPlan) -> ExecutionPlan {
3694        self.try_resident_dependency_closed_plan(plan)
3695            .unwrap_or_else(|| plan.clone())
3696    }
3697
3698    fn try_resident_dependency_closed_plan(&self, plan: &ExecutionPlan) -> Option<ExecutionPlan> {
3699        if self.program.queries.is_empty()
3700            || plan.generated_query_rules.len() != self.program.queries.len()
3701        {
3702            return None;
3703        }
3704
3705        let mut roots =
3706            Vec::with_capacity(plan.generated_query_rules.len() + self.program.constraints.len());
3707        let mut root_heads = std::collections::HashSet::with_capacity(roots.capacity());
3708        let mut seen_queries = vec![false; self.program.queries.len()];
3709        for query in &plan.generated_query_rules {
3710            let expected_head = format!("__xlog_query_{}", query.query_index);
3711            let rule = plan
3712                .rules_by_scc
3713                .get(query.scc_index)?
3714                .get(query.rule_index)?;
3715            let seen = seen_queries.get_mut(query.query_index)?;
3716            if *seen || rule.head != expected_head {
3717                return None;
3718            }
3719            let occurrences = plan
3720                .rules_by_scc
3721                .iter()
3722                .flatten()
3723                .filter(|candidate| candidate.head == expected_head)
3724                .count();
3725            if occurrences != 1 {
3726                return None;
3727            }
3728            *seen = true;
3729            roots.push(query.scc_index);
3730            if !root_heads.insert(expected_head) {
3731                return None;
3732            }
3733        }
3734        if seen_queries.iter().any(|seen| !seen) {
3735            return None;
3736        }
3737
3738        for constraint_index in 0..self.program.constraints.len() {
3739            let expected_head = format!("__xlog_constraint_{constraint_index}");
3740            let positions = plan
3741                .rules_by_scc
3742                .iter()
3743                .enumerate()
3744                .flat_map(|(scc_index, rules)| {
3745                    rules
3746                        .iter()
3747                        .filter(|rule| rule.head == expected_head)
3748                        .map(move |_| scc_index)
3749                })
3750                .collect::<Vec<_>>();
3751            let [scc_index] = positions.as_slice() else {
3752                return None;
3753            };
3754            roots.push(*scc_index);
3755            if !root_heads.insert(expected_head) {
3756                return None;
3757            }
3758        }
3759
3760        let mut defining_sccs = HashMap::new();
3761        for (scc_index, rules) in plan.rules_by_scc.iter().enumerate() {
3762            for rule in rules {
3763                let Some(relation) = self.rel_ids.get(&rule.head).copied() else {
3764                    if root_heads.contains(&rule.head) {
3765                        continue;
3766                    }
3767                    return None;
3768                };
3769                match defining_sccs.insert(relation, scc_index) {
3770                    Some(previous) if previous != scc_index => return None,
3771                    _ => {}
3772                }
3773            }
3774        }
3775
3776        plan.dependency_closed_subplan(&roots, &defining_sccs)
3777    }
3778
3779    #[cfg(test)]
3780    fn resident_certification_initializations(&self) -> u64 {
3781        self.reusable_state_identity
3782            .resident_certification_initializations()
3783    }
3784
3785    /// Evaluate the program with optional profiling
3786    ///
3787    /// # Arguments
3788    /// * `provider` - The CUDA kernel provider
3789    /// * `inputs` - Input relations
3790    /// * `profiling` - Whether to collect execution statistics
3791    pub fn evaluate_with_options(
3792        &self,
3793        provider: Arc<CudaKernelProvider>,
3794        inputs: HashMap<String, CudaBuffer>,
3795        profiling: bool,
3796    ) -> Result<LogicEvalResult> {
3797        self.reject_compiler_generated_query_relation_names(
3798            inputs.keys().map(String::as_str),
3799            "caller input",
3800        )?;
3801        let resident_mode = ResidentSelectionMode::from_env()?;
3802        if matches!(&self.plan, LogicExecutionPlan::Ordinary(_)) && resident_mode.enabled() {
3803            return self.evaluate_ordinary_with_resident_mode(
3804                provider,
3805                inputs,
3806                profiling,
3807                resident_mode,
3808            );
3809        }
3810        let mut executor = self.prepare_executor(&provider, inputs, profiling)?;
3811
3812        if let LogicExecutionPlan::EpistemicG91Compatibility(g91_plan) = &self.plan {
3813            let result = self
3814                .evaluate_g91_compatibility_gpu_program(provider, executor, g91_plan, profiling)?;
3815            return self.finish_nonordinary_resident_selection(result, resident_mode);
3816        }
3817
3818        if let LogicExecutionPlan::EpistemicWfsGpu(wfs_plan) = &self.plan {
3819            let result = self.evaluate_wfs_gpu_program(provider, executor, wfs_plan, profiling)?;
3820            return self.finish_nonordinary_resident_selection(result, resident_mode);
3821        }
3822
3823        let LogicExecutionPlan::Ordinary(plan) = &self.plan else {
3824            let result = self.evaluate_epistemic_with_executor(&provider, executor, profiling)?;
3825            return self.finish_nonordinary_resident_selection(result, resident_mode);
3826        };
3827
3828        executor.execute_plan(plan)?;
3829
3830        self.enforce_constraints(&provider, &executor)?;
3831
3832        let mut queries: Vec<LogicQueryResult> = Vec::with_capacity(self.program.queries.len());
3833        for (i, query) in self.program.queries.iter().enumerate() {
3834            let internal_relation_name = format!("__xlog_query_{}", i);
3835            let buffer = executor
3836                .store_mut()
3837                .remove(&internal_relation_name)
3838                .ok_or_else(|| {
3839                    XlogError::Execution(format!(
3840                        "Missing query result relation {} (compiler bug?)",
3841                        internal_relation_name
3842                    ))
3843                })?;
3844
3845            queries.push(self.logic_query_result(
3846                provider.as_ref(),
3847                i,
3848                query,
3849                internal_relation_name,
3850                buffer,
3851            )?);
3852        }
3853
3854        let stats = if profiling {
3855            let total_output_rows =
3856                total_logical_rows(provider.as_ref(), queries.iter().map(|query| &query.buffer))?;
3857            Some(executor.execution_stats(total_output_rows))
3858        } else {
3859            None
3860        };
3861
3862        Ok(LogicEvalResult { queries, stats })
3863    }
3864
3865    /// Build an executor seeded with declared schemas, caller inputs and program facts.
3866    ///
3867    /// Shared by ordinary evaluation and by the epistemic evidence handoff so both
3868    /// paths seed relations identically.
3869    fn prepare_executor(
3870        &self,
3871        provider: &Arc<CudaKernelProvider>,
3872        inputs: HashMap<String, CudaBuffer>,
3873        profiling: bool,
3874    ) -> Result<Executor> {
3875        self.prepare_executor_excluding_derived_placeholders(provider, inputs, profiling, None)
3876    }
3877
3878    /// Execute an epistemic program and return its accepted GPU execution evidence.
3879    ///
3880    /// `evaluate` reduces the epistemic result to query rows and drops the accepted
3881    /// world-view evidence with it. The probabilistic production adapter needs that
3882    /// evidence itself, so this entry point keeps the raw execution result.
3883    ///
3884    /// Only single-component epistemic plans are supported: split, stratified and WFS
3885    /// plans produce several world views whose probabilistic conditioning contract is
3886    /// not settled yet, so they are rejected loudly rather than silently reduced.
3887    ///
3888    /// "Ordinary" here names a plan kind, not the source. An admissible recursive modal
3889    /// program (`reach(X, Z) :- reach(X, Y), know link(Y, Z).`) is lowered by the Case-A
3890    /// reduction to ordinary recursion: the world-view machinery is erased, so no
3891    /// accepted world view survives to hand over and the program is rejected here
3892    /// despite being full of `know`. The rejection names the `epistemic_provenance`
3893    /// reduction class so the message explains the lowering instead of reading as a
3894    /// misclassification.
3895    pub fn execute_epistemic_evidence(
3896        &self,
3897        provider: Arc<CudaKernelProvider>,
3898        inputs: HashMap<String, CudaBuffer>,
3899    ) -> Result<EpistemicGpuExecutionResult> {
3900        let LogicExecutionPlan::EpistemicSingle(executable) = &self.plan else {
3901            let reduction = self
3902                .epistemic_provenance
3903                .as_ref()
3904                .map(|provenance| provenance.reduction)
3905                .unwrap_or("none");
3906            return Err(XlogError::UnsupportedEpistemicConstruct {
3907                construct: "epistemic accepted-evidence handoff".to_string(),
3908                context: format!(
3909                    "execute_epistemic_evidence requires a single-component epistemic plan; \
3910                     ordinary, split, stratified, recursive G91-compatibility and WFS plans \
3911                     are not supported (epistemic_provenance reduction: {reduction}). A \
3912                     recursive modal program reduced to ordinary recursion \
3913                     (ordinary_recursive_modal_reduction) is rejected here by design: the \
3914                     reduction erases the world-view machinery, so no accepted world view \
3915                     survives to condition on"
3916                ),
3917            });
3918        };
3919
3920        let mut executor = self.prepare_executor(&provider, inputs, false)?;
3921        let result = executor
3922            .execute_epistemic_gpu_execution(
3923                executable,
3924                capacities_for_epistemic_executable(executable)?,
3925            )
3926            .map_err(|error| self.present_epistemic_constraint_violation(error))?;
3927        result.require_runtime_dispatch_certification()?;
3928        Ok(result)
3929    }
3930
3931    /// Compare query result relations between two stores using GPU set difference.
3932    pub fn relation_stores_query_equivalent(
3933        &self,
3934        provider: &CudaKernelProvider,
3935        left: &RelationStore,
3936        right: &RelationStore,
3937    ) -> Result<bool> {
3938        for idx in 0..self.program.queries.len() {
3939            let name = format!("__xlog_query_{}", idx);
3940            let Some(left_buffer) = left.get(&name) else {
3941                return Ok(false);
3942            };
3943            let Some(right_buffer) = right.get(&name) else {
3944                return Ok(false);
3945            };
3946            if !buffers_gpu_set_equivalent(provider, left_buffer, right_buffer)? {
3947                return Ok(false);
3948            }
3949        }
3950        Ok(true)
3951    }
3952
3953    fn clone_relation_store(
3954        &self,
3955        provider: &Arc<CudaKernelProvider>,
3956        source: &RelationStore,
3957    ) -> Result<RelationStore> {
3958        let mut cloned = RelationStore::new(provider.clone());
3959        for name in source.names() {
3960            let buffer = source.get(name).ok_or_else(|| {
3961                XlogError::Execution(format!("Relation {} disappeared during clone", name))
3962            })?;
3963            cloned.put(name, provider.clone_buffer(buffer)?);
3964        }
3965        Ok(cloned)
3966    }
3967
3968    fn clone_prepared_relation_snapshot(
3969        &self,
3970        provider: &Arc<CudaKernelProvider>,
3971        source: &RelationStore,
3972    ) -> Result<RelationStore> {
3973        let mut relation_names = source.names().collect::<Vec<_>>();
3974        relation_names.sort_unstable();
3975        let mut cloned = RelationStore::new(provider.clone());
3976        cloned.try_reserve_relations(relation_names.len())?;
3977        for name in relation_names {
3978            let buffer = source.get(name).ok_or_else(|| {
3979                XlogError::Execution(format!(
3980                    "Relation {name} disappeared while cloning prepared snapshot"
3981                ))
3982            })?;
3983            let context = format!("cloning prospective relation snapshot '{name}'");
3984            let cloned_buffer = provider
3985                .clone_buffer(buffer)
3986                .map_err(|error| relation_clone_error(context, error))?;
3987            cloned.put(name, cloned_buffer);
3988        }
3989        Ok(cloned)
3990    }
3991
3992    fn total_query_rows(
3993        &self,
3994        provider: &CudaKernelProvider,
3995        store: &RelationStore,
3996    ) -> Result<u64> {
3997        (0..self.program.queries.len()).try_fold(0_u64, |total, index| {
3998            let relation_name = format!("__xlog_query_{index}");
3999            let buffer = store.get(&relation_name).ok_or_else(|| {
4000                XlogError::Execution(format!(
4001                    "Missing query result relation {relation_name} (compiler bug?)"
4002                ))
4003            })?;
4004            add_logical_row_count(provider, total, buffer)
4005        })
4006    }
4007
4008    fn logic_result_from_store(
4009        &self,
4010        provider: &CudaKernelProvider,
4011        store: &RelationStore,
4012        stats: Option<ExecutionStats>,
4013    ) -> Result<LogicEvalResult> {
4014        let mut queries: Vec<LogicQueryResult> = Vec::with_capacity(self.program.queries.len());
4015        for (i, query) in self.program.queries.iter().enumerate() {
4016            let relation_name = format!("__xlog_query_{}", i);
4017            let buffer = store.get(&relation_name).ok_or_else(|| {
4018                XlogError::Execution(format!(
4019                    "Missing query result relation {} (compiler bug?)",
4020                    relation_name
4021                ))
4022            })?;
4023
4024            queries.push(self.logic_query_result(
4025                provider,
4026                i,
4027                query,
4028                relation_name,
4029                provider.clone_buffer(buffer)?,
4030            )?);
4031        }
4032
4033        Ok(LogicEvalResult { queries, stats })
4034    }
4035
4036    fn logic_query_result(
4037        &self,
4038        provider: &CudaKernelProvider,
4039        query_index: usize,
4040        query: &Query,
4041        internal_relation_name: String,
4042        buffer: CudaBuffer,
4043    ) -> Result<LogicQueryResult> {
4044        let provenance = self.epistemic_provenance.as_ref();
4045        let surface_source_query = provenance.is_some_and(|value| value.surface_source_queries);
4046        let presentation_query = if surface_source_query {
4047            self.source_program
4048                .queries
4049                .get(query_index)
4050                .ok_or_else(|| {
4051                    XlogError::Execution(format!(
4052                        "missing authored metadata for query {query_index}"
4053                    ))
4054                })?
4055        } else {
4056            query
4057        };
4058        let columns = query_output_vars(presentation_query);
4059        let buffer = if columns.is_empty() {
4060            let row_count = provider.device_row_count(&buffer)?;
4061            let row_count = u32::try_from(row_count).map_err(|_| {
4062                XlogError::Execution(format!(
4063                    "query result row count {row_count} exceeds the GPU row-count range"
4064                ))
4065            })?;
4066            provider.create_zero_arity_buffer(Schema::new(Vec::new()), row_count)?
4067        } else {
4068            buffer
4069        };
4070        let relation_name = if surface_source_query {
4071            presentation_query.atom.predicate.clone()
4072        } else {
4073            internal_relation_name
4074        };
4075
4076        Ok(LogicQueryResult {
4077            relation_name,
4078            sort_labels: columns.clone(),
4079            columns,
4080            buffer,
4081        })
4082    }
4083
4084    fn arity_qualified_fact_predicates(&self) -> &BTreeSet<String> {
4085        self.fact_arity_qualifications.get_or_init(|| {
4086            if self.epistemic_provenance.is_some() {
4087                epistemic_extensional_multi_arity_predicates(&self.program)
4088            } else {
4089                predicate_arities(&self.program)
4090                    .into_iter()
4091                    .filter_map(|(predicate, arities)| (arities.len() > 1).then_some(predicate))
4092                    .collect()
4093            }
4094        })
4095    }
4096
4097    fn fact_rows_by_relation<'program>(
4098        &'program self,
4099        arity_qualified_predicates: &BTreeSet<String>,
4100    ) -> HashMap<String, Vec<&'program [Term]>> {
4101        let mut rows_by_pred = HashMap::new();
4102        for fact in self.program.facts() {
4103            let predicate = fact.head.predicate.as_str();
4104            let relation = if arity_qualified_predicates.contains(predicate) {
4105                arity_qualified_name(predicate, fact.head.terms.len())
4106            } else {
4107                predicate.to_string()
4108            };
4109            rows_by_pred
4110                .entry(relation)
4111                .or_insert_with(Vec::new)
4112                .push(fact.head.terms.as_slice());
4113        }
4114        rows_by_pred
4115    }
4116
4117    fn load_facts_into_store(
4118        &self,
4119        provider: &CudaKernelProvider,
4120        store: &mut RelationStore,
4121    ) -> Result<()> {
4122        let arity_qualified_predicates = self.arity_qualified_fact_predicates();
4123        let rows_by_pred = self.fact_rows_by_relation(arity_qualified_predicates);
4124        self.load_grouped_facts_into_store(provider, store, rows_by_pred, &BTreeSet::new())
4125    }
4126
4127    fn host_fact_row_equality_matches_union(schema: &Schema) -> bool {
4128        // The GPU's unary float dedup equates signed zeros. Multi-column set
4129        // dedup uses full-row encoded-byte equality instead.
4130        schema.arity() != 1
4131            || !matches!(
4132                schema.column_type(0),
4133                Some(ScalarType::F32 | ScalarType::F64)
4134            )
4135    }
4136
4137    fn host_fact_rows_can_be_canonicalized(schema: &Schema) -> bool {
4138        (0..schema.arity()).all(|index| {
4139            matches!(
4140                schema.column_type(index),
4141                Some(ScalarType::U32 | ScalarType::Symbol)
4142            )
4143        })
4144    }
4145
4146    fn load_grouped_facts_into_store(
4147        &self,
4148        provider: &CudaKernelProvider,
4149        store: &mut RelationStore,
4150        rows_by_pred: HashMap<String, Vec<&[Term]>>,
4151        directly_loaded_fact_names: &BTreeSet<String>,
4152    ) -> Result<()> {
4153        let mut names = Vec::with_capacity(rows_by_pred.len());
4154        let mut host_relations = Vec::with_capacity(rows_by_pred.len());
4155        for (pred, rows) in rows_by_pred {
4156            let schema = self.schemas.get(pred.as_str()).ok_or_else(|| {
4157                XlogError::Execution(format!(
4158                    "Missing inferred schema for fact predicate {}",
4159                    pred
4160                ))
4161            })?;
4162
4163            if rows.iter().any(|r| r.len() != schema.arity()) {
4164                return Err(XlogError::Execution(format!(
4165                    "Fact arity mismatch for {} (expected {} columns)",
4166                    pred,
4167                    schema.arity()
4168                )));
4169            }
4170
4171            let mut seen_rows = Self::host_fact_row_equality_matches_union(schema)
4172                .then(|| HashSet::with_capacity(rows.len()));
4173            let mut fact_rows = 0;
4174            let mut columns: Vec<Vec<u8>> = vec![Vec::new(); schema.arity()];
4175            let mut column_starts = Vec::with_capacity(schema.arity());
4176            for row in rows {
4177                column_starts.clear();
4178                let mut row_key = Vec::with_capacity(schema.row_size_bytes());
4179                for (col_idx, term) in row.iter().enumerate() {
4180                    let typ = schema.column_type(col_idx).ok_or_else(|| {
4181                        XlogError::Execution(format!("Missing type for column {}", col_idx))
4182                    })?;
4183                    column_starts.push(columns[col_idx].len());
4184                    append_ground_term_bytes(&mut columns[col_idx], term, typ).map_err(|error| {
4185                        XlogError::Execution(format!(
4186                            "Failed to encode fact for predicate {pred} at column {col_idx}: {error}"
4187                        ))
4188                    })?;
4189                    if seen_rows.is_some() {
4190                        row_key.extend_from_slice(&columns[col_idx][column_starts[col_idx]..]);
4191                    }
4192                }
4193                if seen_rows.as_mut().is_some_and(|seen| !seen.insert(row_key)) {
4194                    for (column, start) in columns.iter_mut().zip(&column_starts) {
4195                        column.truncate(*start);
4196                    }
4197                } else {
4198                    fact_rows += 1;
4199                }
4200            }
4201            if directly_loaded_fact_names.contains(&pred) && fact_rows > 1 {
4202                let mut order = (0..fact_rows).collect::<Vec<_>>();
4203                order.sort_unstable_by(|&left, &right| {
4204                    columns
4205                        .iter()
4206                        .map(|column| {
4207                            let left_start = left * std::mem::size_of::<u32>();
4208                            let right_start = right * std::mem::size_of::<u32>();
4209                            let left_value = u32::from_le_bytes(
4210                                column[left_start..left_start + 4]
4211                                    .try_into()
4212                                    .expect("u32 fact column"),
4213                            );
4214                            let right_value = u32::from_le_bytes(
4215                                column[right_start..right_start + 4]
4216                                    .try_into()
4217                                    .expect("u32 fact column"),
4218                            );
4219                            left_value.cmp(&right_value)
4220                        })
4221                        .find(|ordering| *ordering != CmpOrdering::Equal)
4222                        .unwrap_or(CmpOrdering::Equal)
4223                });
4224                for column in &mut columns {
4225                    let mut sorted = Vec::with_capacity(column.len());
4226                    for &row in &order {
4227                        let start = row * std::mem::size_of::<u32>();
4228                        sorted.extend_from_slice(&column[start..start + 4]);
4229                    }
4230                    *column = sorted;
4231                }
4232            }
4233            // Every asserted nullary fact denotes the same unit tuple.
4234            names.push(pred);
4235            host_relations.push((schema.clone(), columns, fact_rows));
4236        }
4237
4238        for (pred, fact_buf) in names
4239            .into_iter()
4240            .zip(provider.create_buffers_from_host_columns(&host_relations)?)
4241        {
4242            if directly_loaded_fact_names.contains(&pred) {
4243                store.put(pred.as_str(), fact_buf);
4244                continue;
4245            }
4246            let existing = store.get(&pred).ok_or_else(|| {
4247                XlogError::Execution(format!(
4248                    "Missing base relation {} while loading facts",
4249                    pred
4250                ))
4251            })?;
4252
4253            let merged = provider.union(existing, &fact_buf)?;
4254            store.put(pred.as_str(), merged);
4255        }
4256
4257        Ok(())
4258    }
4259
4260    fn evaluate_wfs_gpu_program(
4261        &self,
4262        provider: Arc<CudaKernelProvider>,
4263        base_executor: Executor,
4264        wfs: &EpistemicWfsGpuPlan,
4265        profiling: bool,
4266    ) -> Result<LogicEvalResult> {
4267        let base_store = self.clone_relation_store(&provider, base_executor.store())?;
4268        let mut stats = profiling.then(ExecutionStats::default);
4269        let lower_store =
4270            self.run_wfs_gpu_fixpoint(&provider, &base_store, wfs, profiling, &mut stats)?;
4271        self.enforce_constraints_in_store(provider.as_ref(), &lower_store)?;
4272        if stats.is_some() {
4273            let total_output_rows = self.total_query_rows(provider.as_ref(), &lower_store)?;
4274            finalize_iterative_execution_stats(&mut stats, total_output_rows);
4275        }
4276        self.logic_result_from_store(provider.as_ref(), &lower_store, stats)
4277    }
4278
4279    fn run_wfs_gpu_fixpoint(
4280        &self,
4281        provider: &Arc<CudaKernelProvider>,
4282        base_store: &RelationStore,
4283        wfs: &EpistemicWfsGpuPlan,
4284        profiling: bool,
4285        stats: &mut Option<ExecutionStats>,
4286    ) -> Result<RelationStore> {
4287        let upper_executor =
4288            self.run_gpu_ordinary_pass(provider, &wfs.overapprox, base_store, &[], profiling)?;
4289        collect_iterative_execution_stats(stats, &upper_executor);
4290        let mut upper_store = self.clone_relation_store(provider, upper_executor.store())?;
4291        let mut lower_store = self.clone_relation_store(provider, base_store)?;
4292
4293        for _ in 0..wfs.max_iterations {
4294            let upper_fixed: Vec<_> = wfs
4295                .upper_fixed_names
4296                .iter()
4297                .map(|(source, fixed)| (source.as_str(), fixed.as_str(), &upper_store))
4298                .collect();
4299            let lower_executor = self.run_gpu_ordinary_pass(
4300                provider,
4301                &wfs.lower,
4302                base_store,
4303                &upper_fixed,
4304                profiling,
4305            )?;
4306            collect_iterative_execution_stats(stats, &lower_executor);
4307            let next_lower = self.clone_relation_store(provider, lower_executor.store())?;
4308
4309            let lower_fixed: Vec<_> = wfs
4310                .lower_fixed_names
4311                .iter()
4312                .map(|(source, fixed)| (source.as_str(), fixed.as_str(), &next_lower))
4313                .collect();
4314            let next_upper_executor = self.run_gpu_ordinary_pass(
4315                provider,
4316                &wfs.upper,
4317                base_store,
4318                &lower_fixed,
4319                profiling,
4320            )?;
4321            collect_iterative_execution_stats(stats, &next_upper_executor);
4322            let next_upper = self.clone_relation_store(provider, next_upper_executor.store())?;
4323
4324            let lower_converged =
4325                self.wfs_gpu_stores_equivalent(provider, wfs, &lower_store, &next_lower)?;
4326            let upper_converged =
4327                self.wfs_gpu_stores_equivalent(provider, wfs, &upper_store, &next_upper)?;
4328            lower_store = next_lower;
4329            upper_store = next_upper;
4330            if lower_converged && upper_converged {
4331                return Ok(lower_store);
4332            }
4333        }
4334
4335        Err(XlogError::Execution(format!(
4336            "GPU-backed WFS did not converge within {} alternating-fixpoint iterations; raise \
4337             #pragma max_recursion_depth only when the finite relation domain requires it",
4338            wfs.max_iterations
4339        )))
4340    }
4341
4342    fn evaluate_g91_compatibility_gpu_program(
4343        &self,
4344        provider: Arc<CudaKernelProvider>,
4345        base_executor: Executor,
4346        g91: &EpistemicG91CompatibilityGpuPlan,
4347        profiling: bool,
4348    ) -> Result<LogicEvalResult> {
4349        let base_store = self.clone_relation_store(&provider, base_executor.store())?;
4350        let mut stats = profiling.then(ExecutionStats::default);
4351        let mut current_store = self.run_gpu_evaluation_pass(
4352            &provider,
4353            &g91.upper_bound,
4354            &base_store,
4355            &[],
4356            profiling,
4357            &mut stats,
4358        )?;
4359        let refinement_schemas = gpu_evaluation_pass_schemas(&g91.refinement);
4360
4361        for _ in 0..g91.max_iterations {
4362            let snapshots = g91
4363                .snapshot_relations
4364                .iter()
4365                .map(|(source, snapshot)| (source.as_str(), snapshot.as_str(), &current_store))
4366                .collect::<Vec<_>>();
4367            let next_store = self.run_gpu_evaluation_pass(
4368                &provider,
4369                &g91.refinement,
4370                &base_store,
4371                &snapshots,
4372                profiling,
4373                &mut stats,
4374            )?;
4375            let converged = self.gpu_stores_equivalent(
4376                &provider,
4377                &refinement_schemas,
4378                &g91.convergence_predicates,
4379                &current_store,
4380                &next_store,
4381            )?;
4382            if converged {
4383                self.enforce_constraints_in_store(&provider, &next_store)?;
4384                if stats.is_some() {
4385                    let total_output_rows =
4386                        self.total_query_rows(provider.as_ref(), &next_store)?;
4387                    finalize_iterative_execution_stats(&mut stats, total_output_rows);
4388                }
4389                return self.logic_result_from_store(provider.as_ref(), &next_store, stats);
4390            }
4391            current_store = next_store;
4392        }
4393
4394        Err(XlogError::Execution(format!(
4395            "Gelfond-1991 tuple compatibility did not converge within {} refinement iterations; \
4396             raise #pragma max_recursion_depth only when the finite relation domain requires it",
4397            g91.max_iterations
4398        )))
4399    }
4400
4401    fn run_gpu_ordinary_pass(
4402        &self,
4403        provider: &Arc<CudaKernelProvider>,
4404        pass: &GpuOrdinaryPass,
4405        base_store: &RelationStore,
4406        fixed_relations: &[(&str, &str, &RelationStore)],
4407        profiling: bool,
4408    ) -> Result<Executor> {
4409        let mut executor = Executor::new(provider.clone());
4410        executor.set_profiling(profiling);
4411        for (name, rel_id) in &pass.rel_ids {
4412            executor.register_relation(*rel_id, name);
4413        }
4414        for (name, schema) in &pass.schemas {
4415            executor
4416                .store_mut()
4417                .put(name, provider.create_empty_buffer(schema.clone())?);
4418        }
4419        for name in base_store.names() {
4420            // Query and constraint relations are outputs of this compiled pass.
4421            // Seeding them from an earlier epistemic stratum would union stale,
4422            // ungated candidates into the authoritative recomputation.
4423            if pass.schemas.contains_key(name)
4424                && !name.starts_with("__xlog_query_")
4425                && !name.starts_with("__xlog_constraint_")
4426            {
4427                let buffer = base_store.get(name).ok_or_else(|| {
4428                    XlogError::Execution(format!("WFS base relation {name} disappeared"))
4429                })?;
4430                executor
4431                    .store_mut()
4432                    .put(name, provider.clone_buffer(buffer)?);
4433            }
4434        }
4435        for &(source, fixed, source_store) in fixed_relations {
4436            let buffer =
4437                self.gpu_clone_or_empty(provider, &pass.schemas, source, fixed, source_store)?;
4438            executor.store_mut().put(fixed, buffer);
4439        }
4440        executor.execute_plan(&pass.plan)?;
4441        Ok(executor)
4442    }
4443
4444    fn run_gpu_evaluation_pass(
4445        &self,
4446        provider: &Arc<CudaKernelProvider>,
4447        pass: &GpuEvaluationPass,
4448        base_store: &RelationStore,
4449        fixed_relations: &[(&str, &str, &RelationStore)],
4450        profiling: bool,
4451        stats: &mut Option<ExecutionStats>,
4452    ) -> Result<RelationStore> {
4453        match pass {
4454            GpuEvaluationPass::Ordinary(ordinary) => {
4455                let executor = self.run_gpu_ordinary_pass(
4456                    provider,
4457                    ordinary,
4458                    base_store,
4459                    fixed_relations,
4460                    profiling,
4461                )?;
4462                collect_iterative_execution_stats(stats, &executor);
4463                self.clone_relation_store(provider, executor.store())
4464            }
4465            GpuEvaluationPass::Wfs(wfs) => {
4466                let schemas = wfs_plan_combined_schemas(wfs);
4467                let mut pass_base = self.clone_relation_store(provider, base_store)?;
4468                for &(source, fixed, source_store) in fixed_relations {
4469                    let buffer =
4470                        self.gpu_clone_or_empty(provider, &schemas, source, fixed, source_store)?;
4471                    pass_base.put(fixed, buffer);
4472                }
4473                self.run_wfs_gpu_fixpoint(provider, &pass_base, wfs, profiling, stats)
4474            }
4475        }
4476    }
4477
4478    fn gpu_clone_or_empty(
4479        &self,
4480        provider: &Arc<CudaKernelProvider>,
4481        schemas: &HashMap<String, Schema>,
4482        source_name: &str,
4483        target_name: &str,
4484        store: &RelationStore,
4485    ) -> Result<CudaBuffer> {
4486        let target_schema = schemas
4487            .get(target_name)
4488            .or_else(|| self.schemas.get(target_name))
4489            .ok_or_else(|| {
4490                XlogError::Execution(format!(
4491                    "missing iterative GPU relation schema for {target_name}"
4492                ))
4493            })?;
4494        if let Some(buffer) = store.get(source_name) {
4495            // Nullary fixed relations use a unary unit marker inside WFS passes.
4496            // CUDA anti-join kernels then distinguish the absent and present unit
4497            // tuples using their ordinary nonzero-arity path.
4498            if buffer.schema().arity() == 0 && target_schema.arity() == 1 {
4499                if provider.device_row_count(buffer)? == 0 {
4500                    return provider.create_empty_buffer(target_schema.clone());
4501                }
4502                let marker = 1u32.to_le_bytes();
4503                return provider
4504                    .create_buffer_from_slices(&[marker.as_slice()], target_schema.clone());
4505            }
4506            return provider.clone_buffer(buffer);
4507        }
4508        provider.create_empty_buffer(target_schema.clone())
4509    }
4510
4511    fn wfs_gpu_stores_equivalent(
4512        &self,
4513        provider: &Arc<CudaKernelProvider>,
4514        wfs: &EpistemicWfsGpuPlan,
4515        left: &RelationStore,
4516        right: &RelationStore,
4517    ) -> Result<bool> {
4518        self.gpu_stores_equivalent(
4519            provider,
4520            &wfs.lower.schemas,
4521            &wfs.intensional_predicates,
4522            left,
4523            right,
4524        )
4525    }
4526
4527    fn gpu_stores_equivalent(
4528        &self,
4529        provider: &Arc<CudaKernelProvider>,
4530        schemas: &HashMap<String, Schema>,
4531        predicates: &[String],
4532        left: &RelationStore,
4533        right: &RelationStore,
4534    ) -> Result<bool> {
4535        for pred in predicates {
4536            let left_buf = self.gpu_clone_or_empty(provider, schemas, pred, pred, left)?;
4537            let right_buf = self.gpu_clone_or_empty(provider, schemas, pred, pred, right)?;
4538            if !buffers_gpu_set_equivalent(provider.as_ref(), &left_buf, &right_buf)? {
4539                return Ok(false);
4540            }
4541        }
4542        Ok(true)
4543    }
4544
4545    fn ordinary_plan(&self, context: &str) -> Result<&ExecutionPlan> {
4546        match &self.plan {
4547            LogicExecutionPlan::Ordinary(plan) => Ok(plan),
4548            LogicExecutionPlan::EpistemicG91Compatibility(_)
4549            | LogicExecutionPlan::EpistemicWfsGpu(_)
4550            | LogicExecutionPlan::EpistemicSingle(_)
4551            | LogicExecutionPlan::EpistemicSplit(_)
4552            | LogicExecutionPlan::EpistemicStratified(_) => {
4553                Err(XlogError::UnsupportedEpistemicConstruct {
4554                    construct: "epistemic high-level persistent execution".to_string(),
4555                    context: format!(
4556                        "{context} requires an ordinary RIR plan; use evaluate/evaluate_with_options \
4557                         for production epistemic GPU dispatch"
4558                    ),
4559                })
4560            }
4561        }
4562    }
4563
4564    fn evaluate_epistemic_with_executor(
4565        &self,
4566        provider: &Arc<CudaKernelProvider>,
4567        mut executor: Executor,
4568        profiling: bool,
4569    ) -> Result<LogicEvalResult> {
4570        let mut queries = Vec::new();
4571        let mut accumulated_stats = None;
4572        match &self.plan {
4573            LogicExecutionPlan::EpistemicSingle(executable) => {
4574                let result = executor
4575                    .execute_epistemic_gpu_execution(
4576                        executable,
4577                        capacities_for_epistemic_executable(executable)?,
4578                    )
4579                    .map_err(|error| self.present_epistemic_constraint_violation(error))?;
4580                result.require_runtime_dispatch_certification()?;
4581                queries.extend(epistemic_result_to_query_results(
4582                    epistemic_output_relation_name(executable)?,
4583                    result,
4584                ));
4585            }
4586            LogicExecutionPlan::EpistemicSplit(split) => {
4587                let executables: Vec<_> = split
4588                    .components
4589                    .iter()
4590                    .map(|component| &component.executable)
4591                    .collect();
4592                let batch = executor
4593                    .execute_epistemic_gpu_execution_batch_with_trace(
4594                        &executables,
4595                        capacities_for_epistemic_split(split)?,
4596                    )
4597                    .map_err(|error| self.present_epistemic_constraint_violation(error))?;
4598                batch
4599                    .require_trace_matches_components("xlog high-level epistemic GPU execution")?;
4600                for result in &batch.results {
4601                    result.require_runtime_dispatch_certification()?;
4602                }
4603                for (component, result) in split.components.iter().zip(batch.results) {
4604                    // A JOINT-SOLVED coalesced multi-head component yields one query
4605                    // per coupled head: the primary head from `final_output` plus
4606                    // each additional head materialized against the SAME accepted
4607                    // world view. Single-head components yield exactly one query.
4608                    queries.extend(epistemic_result_to_query_results(
4609                        epistemic_output_relation_name(&component.executable)?,
4610                        result,
4611                    ));
4612                }
4613            }
4614            LogicExecutionPlan::EpistemicStratified(stratified) => {
4615                // Execute strata in topological order on the SAME executor. After
4616                // each stratum, write its GATED head output(s) into the store as
4617                // base relations so the NEXT stratum's `know`/`possible` over a
4618                // lower head reads the gated extension through the existing tuple-key
4619                // membership filter (or, once the head is a materialized base
4620                // relation, Case-A resolve-into-body) — never double-gating against
4621                // a still-modal relation.
4622                //
4623                // Every authored query is evaluated by its compiled query rule only
4624                // after the modal heads and ordinary closure are final. This preserves
4625                // source order, constants, repeated-variable filters, projections and
4626                // logical zero-column truth without exposing whole head relations.
4627                let has_authored_queries = !self.program.queries.is_empty();
4628                let stratum_count = stratified.strata.len();
4629                for (stratum_index, stratum) in stratified.strata.iter().enumerate() {
4630                    let is_last = stratum_index + 1 == stratum_count;
4631                    match &stratum.plan {
4632                        StratumPlanKind::Single(executable) => {
4633                            let result = executor
4634                                .execute_epistemic_gpu_execution(
4635                                    executable,
4636                                    capacities_for_epistemic_executable(executable)?,
4637                                )
4638                                .map_err(|error| {
4639                                    self.present_epistemic_constraint_violation(error)
4640                                })?;
4641                            result.require_runtime_dispatch_certification()?;
4642                            let primary_head = epistemic_output_relation_name(executable)?;
4643                            Self::materialize_epistemic_stratum_result(
4644                                &mut executor,
4645                                primary_head,
4646                                result,
4647                                is_last && !has_authored_queries,
4648                                &mut queries,
4649                            )?;
4650                        }
4651                        StratumPlanKind::Split(split) => {
4652                            let executables: Vec<_> = split
4653                                .components
4654                                .iter()
4655                                .map(|component| &component.executable)
4656                                .collect();
4657                            let batch = executor
4658                                .execute_epistemic_gpu_execution_batch_with_trace(
4659                                    &executables,
4660                                    capacities_for_epistemic_split(split)?,
4661                                )
4662                                .map_err(|error| {
4663                                    self.present_epistemic_constraint_violation(error)
4664                                })?;
4665                            batch.require_trace_matches_components(
4666                                "xlog high-level stratified epistemic GPU execution",
4667                            )?;
4668                            for result in &batch.results {
4669                                result.require_runtime_dispatch_certification()?;
4670                            }
4671                            let primaries: Vec<String> = split
4672                                .components
4673                                .iter()
4674                                .map(|component| {
4675                                    epistemic_output_relation_name(&component.executable)
4676                                })
4677                                .collect::<Result<Vec<_>>>()?;
4678                            for (primary_head, result) in primaries.into_iter().zip(batch.results) {
4679                                Self::materialize_epistemic_stratum_result(
4680                                    &mut executor,
4681                                    primary_head,
4682                                    result,
4683                                    is_last && !has_authored_queries,
4684                                    &mut queries,
4685                                )?;
4686                            }
4687                        }
4688                        StratumPlanKind::Ordinary {
4689                            plan,
4690                            head_predicates,
4691                        } => {
4692                            // Case-A recursive stratum over the materialized base
4693                            // determined head: the ordinary semi-naive engine writes
4694                            // the (correctly gated) head relation into the store.
4695                            executor.execute_plan(plan)?;
4696                            for head in head_predicates {
4697                                if is_last && !has_authored_queries {
4698                                    let buffer =
4699                                        executor.store().get(head.as_str()).ok_or_else(|| {
4700                                            XlogError::Execution(format!(
4701                                            "missing stratified ordinary stratum output relation \
4702                                             {head}"
4703                                        ))
4704                                        })?;
4705                                    let cloned = executor.clone_store_relation(buffer)?;
4706                                    queries.push(epistemic_buffer_to_query_result(
4707                                        head.clone(),
4708                                        cloned,
4709                                    ));
4710                                }
4711                            }
4712                        }
4713                    }
4714                }
4715
4716                // Compiler-local relation IDs cannot safely be registered into the
4717                // modal executor. Run the ordinary closure, authored query rules and
4718                // generated constraint relations in an isolated executor seeded from
4719                // the final gated store, then make that store authoritative.
4720                if profiling {
4721                    accumulated_stats = Some(executor.execution_stats(0));
4722                }
4723                executor = self.run_gpu_ordinary_pass(
4724                    provider,
4725                    &stratified.ordinary_post,
4726                    executor.store(),
4727                    &[],
4728                    profiling,
4729                )?;
4730                if profiling {
4731                    collect_iterative_execution_stats(&mut accumulated_stats, &executor);
4732                }
4733                for (query_index, query) in self.program.queries.iter().enumerate() {
4734                    let internal_relation_name = format!("__xlog_query_{query_index}");
4735                    let buffer = executor
4736                        .store_mut()
4737                        .remove(&internal_relation_name)
4738                        .ok_or_else(|| {
4739                            XlogError::Execution(format!(
4740                                "missing stratified post-stage query relation \
4741                                 {internal_relation_name}"
4742                            ))
4743                        })?;
4744                    queries.push(self.logic_query_result(
4745                        provider,
4746                        query_index,
4747                        query,
4748                        internal_relation_name,
4749                        buffer,
4750                    )?);
4751                }
4752            }
4753            LogicExecutionPlan::EpistemicG91Compatibility(_)
4754            | LogicExecutionPlan::EpistemicWfsGpu(_) => {
4755                unreachable!("iterative GPU epistemic plans are handled earlier")
4756            }
4757            LogicExecutionPlan::Ordinary(_) => {
4758                unreachable!("ordinary plans are handled earlier")
4759            }
4760        }
4761
4762        self.enforce_constraints_in_store(provider, executor.store())?;
4763        let stats = if profiling {
4764            let total_output_rows =
4765                total_logical_rows(provider, queries.iter().map(|query| &query.buffer))?;
4766            if let Some(mut stats) = accumulated_stats {
4767                stats.total_output_rows = total_output_rows;
4768                Some(stats)
4769            } else {
4770                Some(executor.execution_stats(total_output_rows))
4771            }
4772        } else {
4773            None
4774        };
4775        Ok(LogicEvalResult { queries, stats })
4776    }
4777
4778    /// Materialize one epistemic stratum result's GATED head(s) into the store.
4779    ///
4780    /// Every gated head (primary `final_output` plus joint additional heads) is
4781    /// written to the store so higher strata can gate against it. Explicit authored
4782    /// queries are projected from this store after all strata and the ordinary post
4783    /// stage complete.
4784    fn materialize_epistemic_stratum_result(
4785        executor: &mut Executor,
4786        primary_head: String,
4787        result: EpistemicGpuExecutionResult,
4788        surface_default_results: bool,
4789        queries: &mut Vec<LogicQueryResult>,
4790    ) -> Result<()> {
4791        executor.materialize_epistemic_head_relation(&primary_head, &result.final_output)?;
4792        for (head, buffer) in &result.additional_head_outputs {
4793            executor.materialize_epistemic_head_relation(head, buffer)?;
4794        }
4795        if surface_default_results {
4796            queries.extend(epistemic_result_to_query_results(primary_head, result));
4797        }
4798        Ok(())
4799    }
4800
4801    fn enforce_constraints(
4802        &self,
4803        provider: &CudaKernelProvider,
4804        executor: &Executor,
4805    ) -> Result<()> {
4806        self.enforce_constraints_in_store(provider, executor.store())
4807    }
4808
4809    fn constraint_violation_error(&self, constraint_index: usize) -> XlogError {
4810        let presentation_constraint = self
4811            .authored_constraints
4812            .as_ref()
4813            .and_then(|constraints| {
4814                constraints
4815                    .iter()
4816                    .find(|constraint| constraint.authored_index == Some(constraint_index))
4817            })
4818            .or_else(|| {
4819                self.source_program
4820                    .constraints
4821                    .iter()
4822                    .find(|constraint| constraint.authored_index == Some(constraint_index))
4823            })
4824            .or_else(|| {
4825                self.program
4826                    .constraints
4827                    .iter()
4828                    .find(|constraint| constraint.authored_index == Some(constraint_index))
4829            });
4830        let Some(presentation_constraint) = presentation_constraint else {
4831            return XlogError::Execution(format!("Constraint {constraint_index} violated"));
4832        };
4833        XlogError::Execution(format!(
4834            "Constraint {} violated: {}",
4835            constraint_index,
4836            format_constraint_body(&presentation_constraint.body)
4837        ))
4838    }
4839
4840    fn present_epistemic_constraint_violation(&self, error: XlogError) -> XlogError {
4841        match error {
4842            XlogError::ConstraintViolation {
4843                constraint_index, ..
4844            } => self.constraint_violation_error(constraint_index),
4845            other => other,
4846        }
4847    }
4848
4849    fn enforce_constraints_in_store(
4850        &self,
4851        provider: &CudaKernelProvider,
4852        store: &RelationStore,
4853    ) -> Result<()> {
4854        for constraint in &self.program.constraints {
4855            if constraint
4856                .body
4857                .iter()
4858                .any(|literal| matches!(literal, BodyLiteral::Epistemic(_)))
4859            {
4860                continue;
4861            }
4862            let i = constraint.authored_index.ok_or_else(|| {
4863                XlogError::Execution(
4864                    "ordinary constraint reached execution without an authored identity"
4865                        .to_string(),
4866                )
4867            })?;
4868            let name = format!("__xlog_constraint_{i}");
4869            let buf = store.get(&name).ok_or_else(|| {
4870                XlogError::Execution(format!(
4871                    "Missing constraint result relation {} (compiler bug?)",
4872                    name
4873                ))
4874            })?;
4875
4876            if buf.num_rows() == 0 {
4877                continue;
4878            }
4879
4880            let rows = provider.download_column::<u32>(buf, 0).unwrap_or_default();
4881            if rows.is_empty() {
4882                continue;
4883            }
4884
4885            return Err(self.constraint_violation_error(i));
4886        }
4887
4888        Ok(())
4889    }
4890}
4891
4892fn collect_iterative_execution_stats(stats: &mut Option<ExecutionStats>, executor: &Executor) {
4893    let Some(combined) = stats.as_mut() else {
4894        return;
4895    };
4896    let mut pass = executor.execution_stats(0);
4897    let stratum_offset = combined.strata.len();
4898    for (index, stratum) in pass.strata.iter_mut().enumerate() {
4899        stratum.stratum_id = stratum_offset + index;
4900    }
4901    combined.total_duration_us = combined
4902        .total_duration_us
4903        .saturating_add(pass.total_duration_us);
4904    combined.peak_memory_bytes = combined.peak_memory_bytes.max(pass.peak_memory_bytes);
4905    combined.memory_budget_bytes = combined.memory_budget_bytes.max(pass.memory_budget_bytes);
4906    combined.wcoj_triangle_dispatch_count = combined
4907        .wcoj_triangle_dispatch_count
4908        .saturating_add(pass.wcoj_triangle_dispatch_count);
4909    combined.wcoj_4cycle_dispatch_count = combined
4910        .wcoj_4cycle_dispatch_count
4911        .saturating_add(pass.wcoj_4cycle_dispatch_count);
4912    combined.wcoj_groupby_fusion_dispatch_count = combined
4913        .wcoj_groupby_fusion_dispatch_count
4914        .saturating_add(pass.wcoj_groupby_fusion_dispatch_count);
4915    combined.free_join_dispatch_count = combined
4916        .free_join_dispatch_count
4917        .saturating_add(pass.free_join_dispatch_count);
4918    combined.factorized_delta_dispatch_count = combined
4919        .factorized_delta_dispatch_count
4920        .saturating_add(pass.factorized_delta_dispatch_count);
4921    combined.wcoj_error_decline_count = combined
4922        .wcoj_error_decline_count
4923        .saturating_add(pass.wcoj_error_decline_count);
4924    combined
4925        .wcoj_fallback
4926        .saturating_add_assign(pass.wcoj_fallback);
4927    combined.strata.append(&mut pass.strata);
4928}
4929
4930fn finalize_iterative_execution_stats(stats: &mut Option<ExecutionStats>, total_output_rows: u64) {
4931    if let Some(stats) = stats {
4932        stats.total_output_rows = total_output_rows;
4933    }
4934}
4935
4936fn total_logical_rows<'a>(
4937    provider: &CudaKernelProvider,
4938    buffers: impl IntoIterator<Item = &'a CudaBuffer>,
4939) -> Result<u64> {
4940    buffers.into_iter().try_fold(0_u64, |total, buffer| {
4941        add_logical_row_count(provider, total, buffer)
4942    })
4943}
4944
4945fn add_logical_row_count(
4946    provider: &CudaKernelProvider,
4947    total: u64,
4948    buffer: &CudaBuffer,
4949) -> Result<u64> {
4950    let rows = u64::try_from(provider.validated_logical_row_count(buffer)?)
4951        .map_err(|_| XlogError::Execution("logical query row count exceeds u64".into()))?;
4952    total
4953        .checked_add(rows)
4954        .ok_or_else(|| XlogError::Execution("total logical query rows exceed u64".into()))
4955}
4956
4957const DEFAULT_EPISTEMIC_MAX_MODELS_PER_REDUCTION: usize = 1024;
4958
4959/// Normalize a parsed program through the pre-compilation passes used by execution.
4960///
4961/// This helper does not resolve imports; import-aware callers merge them first. It
4962/// expands user-defined functions with the entry program's recursion limit, normalizes
4963/// meta and list builtins, and desugars shared-variable epistemic constraints.
4964pub fn normalize_program_for_execution(mut program: Program) -> Result<Program> {
4965    if program.authored_constraint_source_bound.is_some() {
4966        program.validate_prepared_authored_constraint_identity()?;
4967    } else {
4968        program.prepare_authored_constraint_identity_at_root()?;
4969    }
4970    let max_recursion = program.directives.max_recursion_depth_or_default();
4971    let expanded = xlog_logic::expand_program_functions_owned(program, max_recursion)
4972        .map_err(|e| XlogError::Compilation(e.to_string()))?;
4973    let normalized = xlog_logic::normalize_meta_builtins_owned(expanded)?;
4974    let listed = xlog_logic::normalize_list_builtins_owned(normalized)?;
4975    Ok(desugar_shared_variable_epistemic_constraints(listed))
4976}
4977
4978enum WfsNegationTransform<'a> {
4979    Drop,
4980    Rename {
4981        names: &'a HashMap<String, String>,
4982        source_schemas: &'a HashMap<String, Schema>,
4983    },
4984}
4985
4986fn compile_g91_compatibility_gpu_plan(
4987    reduction: &G91CompatibilityReduction,
4988) -> Result<EpistemicG91CompatibilityGpuPlan> {
4989    let upper_bound = compile_gpu_evaluation_pass(reduction.upper_bound_program())?;
4990    let upper_schemas = gpu_evaluation_pass_schemas(&upper_bound);
4991    let mut refinement_program = reduction.refinement_program().clone();
4992    add_inferred_g91_snapshot_declarations(
4993        &mut refinement_program,
4994        reduction.snapshot_relations(),
4995        &upper_schemas,
4996    )?;
4997    let max_iterations = (refinement_program
4998        .directives
4999        .max_recursion_depth_or_default() as usize)
5000        .max(1);
5001    let refinement = compile_gpu_evaluation_pass(&refinement_program)?;
5002    Ok(EpistemicG91CompatibilityGpuPlan {
5003        upper_bound,
5004        refinement,
5005        snapshot_relations: reduction.snapshot_relations().clone(),
5006        convergence_predicates: reduction.convergence_predicates().to_vec(),
5007        max_iterations,
5008    })
5009}
5010
5011fn compile_gpu_evaluation_pass(program: &Program) -> Result<GpuEvaluationPass> {
5012    let stratification = xlog_logic::stratify::analyze_stratification(program);
5013    if stratification.non_monotone_sccs.is_empty() {
5014        Ok(GpuEvaluationPass::Ordinary(Box::new(
5015            compile_gpu_ordinary_pass(program)?,
5016        )))
5017    } else {
5018        Ok(GpuEvaluationPass::Wfs(Box::new(
5019            compile_epistemic_wfs_gpu_plan(program)?,
5020        )))
5021    }
5022}
5023
5024fn add_inferred_g91_snapshot_declarations(
5025    refinement: &mut Program,
5026    snapshots: &BTreeMap<String, String>,
5027    upper_schemas: &HashMap<String, Schema>,
5028) -> Result<()> {
5029    let existing = refinement
5030        .predicates
5031        .iter()
5032        .map(|declaration| declaration.name.clone())
5033        .collect::<BTreeSet<_>>();
5034    let mut inferred = Vec::new();
5035    for (source, snapshot) in snapshots {
5036        if existing.contains(snapshot) {
5037            continue;
5038        }
5039        let schema =
5040            upper_schemas
5041                .get(source)
5042                .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
5043                    construct: "Gelfond-1991 compatibility snapshot schema".to_string(),
5044                    context: format!(
5045                        "upper-bound compilation produced no schema for compatibility relation \
5046                     `{source}`"
5047                    ),
5048                })?;
5049        let columns = schema
5050            .columns
5051            .iter()
5052            .map(|(name, scalar_type)| PredColumn {
5053                name: Some(name.clone()),
5054                typ: TypeRef::Scalar(*scalar_type),
5055            })
5056            .collect::<Vec<_>>();
5057        inferred.push(PredDecl {
5058            name: snapshot.clone(),
5059            types: columns.iter().map(|column| column.typ.clone()).collect(),
5060            columns,
5061            is_private: false,
5062        });
5063    }
5064    refinement.predicates.extend(inferred);
5065    Ok(())
5066}
5067
5068fn compile_epistemic_wfs_gpu_plan(program: &Program) -> Result<EpistemicWfsGpuPlan> {
5069    let negated = wfs_negated_predicates(program);
5070    let upper_fixed_names = wfs_fixed_names(program, &negated, "__wfs_upper");
5071    let lower_fixed_names = wfs_fixed_names(program, &negated, "__wfs_lower");
5072    let source_schemas = infer_wfs_source_schemas(program)?;
5073
5074    let mut overapprox_program = wfs_transform_program(program, WfsNegationTransform::Drop)?;
5075    // Constraints do not influence the alternating fixpoint. Evaluate them only in
5076    // the lower pass, where positive atoms read the true extension and negated atoms
5077    // read the frozen upper extension.
5078    overapprox_program.constraints.clear();
5079    let lower_program = wfs_transform_program(
5080        program,
5081        WfsNegationTransform::Rename {
5082            names: &upper_fixed_names,
5083            source_schemas: &source_schemas,
5084        },
5085    )?;
5086    let mut upper_program = wfs_transform_program(
5087        program,
5088        WfsNegationTransform::Rename {
5089            names: &lower_fixed_names,
5090            source_schemas: &source_schemas,
5091        },
5092    )?;
5093    upper_program.constraints.clear();
5094
5095    Ok(EpistemicWfsGpuPlan {
5096        overapprox: compile_gpu_ordinary_pass(&overapprox_program)?,
5097        lower: compile_gpu_ordinary_pass(&lower_program)?,
5098        upper: compile_gpu_ordinary_pass(&upper_program)?,
5099        intensional_predicates: wfs_intensional_predicates(program),
5100        upper_fixed_names,
5101        lower_fixed_names,
5102        max_iterations: (program.directives.max_recursion_depth_or_default() as usize).max(1),
5103    })
5104}
5105
5106fn infer_wfs_source_schemas(program: &Program) -> Result<HashMap<String, Schema>> {
5107    // Ordinary compilation cannot plan a cycle through negation, but its schema
5108    // inference treats positive and negated atoms identically. Make a monotone copy
5109    // that retains every atom, compile it through the authoritative compiler path,
5110    // and use the resulting schemas for the private fixed relations.
5111    let mut inference_program = program.clone();
5112    for rule in &mut inference_program.rules {
5113        for literal in &mut rule.body {
5114            if let BodyLiteral::Negated(atom) = literal {
5115                *literal = BodyLiteral::Positive(atom.clone());
5116            }
5117        }
5118    }
5119
5120    let mut compiler = Compiler::new();
5121    compiler.compile_prepared_program(&inference_program)?;
5122    Ok(compiler.schemas().clone())
5123}
5124
5125fn compile_gpu_ordinary_pass(program: &Program) -> Result<GpuOrdinaryPass> {
5126    let mut compiler = Compiler::new();
5127    let plan = compiler.compile_prepared_program(program)?;
5128    Ok(GpuOrdinaryPass {
5129        plan,
5130        schemas: compiler.schemas().clone(),
5131        rel_ids: compiler.rel_ids().clone(),
5132    })
5133}
5134
5135fn wfs_transform_program(program: &Program, negation: WfsNegationTransform<'_>) -> Result<Program> {
5136    let mut out = program.clone();
5137    out.rules = program
5138        .rules
5139        .iter()
5140        .map(|rule| {
5141            let mut rule = rule.clone();
5142            let was_fact = rule.body.is_empty();
5143            let mut body = transform_wfs_body(&rule.body, &negation)?;
5144            if !was_fact && body.is_empty() {
5145                // Dropping every negated literal computes the WFS upper
5146                // over-approximation. Keep the transformed clause executable as a
5147                // unit-derived rule; an empty body would otherwise be reclassified as
5148                // an extensional fact and never derived by the pass.
5149                body.push(BodyLiteral::Comparison(xlog_logic::ast::Comparison {
5150                    left: Term::Integer(1),
5151                    op: xlog_logic::ast::CompOp::Eq,
5152                    right: Term::Integer(1),
5153                }));
5154            }
5155            rule.body = body;
5156            Ok(rule)
5157        })
5158        .collect::<Result<Vec<_>>>()?;
5159    out.constraints = program
5160        .constraints
5161        .iter()
5162        .map(|constraint| {
5163            let mut constraint = constraint.clone();
5164            constraint.body = transform_wfs_body(&constraint.body, &negation)?;
5165            Ok(constraint)
5166        })
5167        .collect::<Result<Vec<_>>>()?;
5168    if let WfsNegationTransform::Rename {
5169        names,
5170        source_schemas,
5171    } = negation
5172    {
5173        add_wfs_fixed_predicates(&mut out, names, source_schemas)?;
5174    }
5175    Ok(out)
5176}
5177
5178fn transform_wfs_body(
5179    body: &[BodyLiteral],
5180    negation: &WfsNegationTransform<'_>,
5181) -> Result<Vec<BodyLiteral>> {
5182    let mut transformed = Vec::with_capacity(body.len());
5183    for literal in body {
5184        match (literal, negation) {
5185            (BodyLiteral::Negated(_), WfsNegationTransform::Drop) => {}
5186            (BodyLiteral::Negated(atom), WfsNegationTransform::Rename { names, .. }) => {
5187                let mut atom = atom.clone();
5188                atom.predicate = names.get(&atom.predicate).cloned().ok_or_else(|| {
5189                    XlogError::Execution(format!(
5190                        "missing WFS fixed relation name for {}",
5191                        atom.predicate
5192                    ))
5193                })?;
5194                if atom.terms.is_empty() {
5195                    atom.terms.push(Term::Integer(1));
5196                }
5197                transformed.push(BodyLiteral::Negated(atom));
5198            }
5199            _ => transformed.push(literal.clone()),
5200        }
5201    }
5202    Ok(transformed)
5203}
5204
5205fn add_wfs_fixed_predicates(
5206    program: &mut Program,
5207    names: &HashMap<String, String>,
5208    source_schemas: &HashMap<String, Schema>,
5209) -> Result<()> {
5210    let existing: BTreeSet<String> = program
5211        .predicates
5212        .iter()
5213        .map(|decl| decl.name.clone())
5214        .collect();
5215    for (source, fixed) in names {
5216        if existing.contains(fixed) {
5217            return Err(XlogError::UnsupportedEpistemicConstruct {
5218                construct: "GPU WFS fixed relation name".to_string(),
5219                context: format!(
5220                    "internal fixed relation {fixed} collides with a declared predicate"
5221                ),
5222            });
5223        }
5224        let Some(schema) = source_schemas.get(source) else {
5225            return Err(XlogError::UnsupportedEpistemicConstruct {
5226                construct: "GPU WFS fixed relation schema".to_string(),
5227                context: format!(
5228                    "ordinary schema inference produced no schema for negated predicate {source}"
5229                ),
5230            });
5231        };
5232
5233        let is_private = program
5234            .predicates
5235            .iter()
5236            .find(|declaration| declaration.name == *source)
5237            .is_some_and(|declaration| declaration.is_private);
5238        let columns = if schema.arity() == 0 {
5239            vec![PredColumn {
5240                name: Some("present".to_string()),
5241                typ: TypeRef::Scalar(ScalarType::U32),
5242            }]
5243        } else {
5244            schema
5245                .columns
5246                .iter()
5247                .map(|(name, scalar_type)| PredColumn {
5248                    name: Some(name.clone()),
5249                    typ: TypeRef::Scalar(*scalar_type),
5250                })
5251                .collect::<Vec<_>>()
5252        };
5253        program.predicates.push(PredDecl {
5254            name: fixed.clone(),
5255            types: columns.iter().map(|column| column.typ.clone()).collect(),
5256            columns,
5257            is_private,
5258        });
5259    }
5260    Ok(())
5261}
5262
5263fn wfs_negated_predicates(program: &Program) -> BTreeSet<String> {
5264    program
5265        .rules
5266        .iter()
5267        .map(|rule| &rule.body)
5268        .chain(
5269            program
5270                .constraints
5271                .iter()
5272                .map(|constraint| &constraint.body),
5273        )
5274        .flatten()
5275        .filter_map(|lit| match lit {
5276            BodyLiteral::Negated(atom) => Some(atom.predicate.clone()),
5277            _ => None,
5278        })
5279        .collect()
5280}
5281
5282fn wfs_intensional_predicates(program: &Program) -> Vec<String> {
5283    program
5284        .proper_rules()
5285        .map(|rule| rule.head.predicate.clone())
5286        .collect::<BTreeSet<_>>()
5287        .into_iter()
5288        .collect()
5289}
5290
5291fn wfs_fixed_names(
5292    program: &Program,
5293    predicates: &BTreeSet<String>,
5294    prefix: &str,
5295) -> HashMap<String, String> {
5296    let mut reserved: BTreeSet<String> = program
5297        .predicates
5298        .iter()
5299        .map(|decl| decl.name.clone())
5300        .collect();
5301    let mut names = HashMap::new();
5302    for pred in predicates {
5303        let mut candidate = format!("{prefix}_{pred}");
5304        if reserved.contains(&candidate) {
5305            let mut suffix = 0usize;
5306            loop {
5307                let suffixed = format!("{prefix}_{suffix}_{pred}");
5308                if !reserved.contains(&suffixed) {
5309                    candidate = suffixed;
5310                    break;
5311                }
5312                suffix += 1;
5313            }
5314        }
5315        reserved.insert(candidate.clone());
5316        names.insert(pred.clone(), candidate);
5317    }
5318    names
5319}
5320
5321fn wfs_plan_combined_schemas(plan: &EpistemicWfsGpuPlan) -> HashMap<String, Schema> {
5322    let mut schemas = HashMap::new();
5323    for ordinary in [&plan.overapprox, &plan.lower, &plan.upper] {
5324        for (name, schema) in &ordinary.schemas {
5325            schemas
5326                .entry(name.clone())
5327                .or_insert_with(|| schema.clone());
5328        }
5329    }
5330    schemas
5331}
5332
5333fn g91_plan_combined_schemas(plan: &EpistemicG91CompatibilityGpuPlan) -> HashMap<String, Schema> {
5334    let mut schemas = HashMap::new();
5335    for pass in [&plan.upper_bound, &plan.refinement] {
5336        for (name, schema) in gpu_evaluation_pass_schemas(pass) {
5337            schemas.entry(name).or_insert(schema);
5338        }
5339    }
5340    schemas
5341}
5342
5343fn gpu_evaluation_pass_schemas(pass: &GpuEvaluationPass) -> HashMap<String, Schema> {
5344    match pass {
5345        GpuEvaluationPass::Ordinary(ordinary) => ordinary.schemas.clone(),
5346        GpuEvaluationPass::Wfs(wfs) => wfs_plan_combined_schemas(wfs),
5347    }
5348}
5349
5350fn g91_plan_combined_rel_ids(plan: &EpistemicG91CompatibilityGpuPlan) -> HashMap<String, RelId> {
5351    let mut rel_ids = HashMap::new();
5352    for pass in [&plan.upper_bound, &plan.refinement] {
5353        for (name, rel_id) in gpu_evaluation_pass_rel_ids(pass) {
5354            rel_ids.insert(name, rel_id);
5355        }
5356    }
5357    rel_ids
5358}
5359
5360fn gpu_evaluation_pass_rel_ids(pass: &GpuEvaluationPass) -> HashMap<String, RelId> {
5361    match pass {
5362        GpuEvaluationPass::Ordinary(ordinary) => ordinary.rel_ids.clone(),
5363        GpuEvaluationPass::Wfs(wfs) => wfs_plan_combined_rel_ids(wfs),
5364    }
5365}
5366
5367fn wfs_plan_combined_rel_ids(plan: &EpistemicWfsGpuPlan) -> HashMap<String, RelId> {
5368    let mut rel_ids = HashMap::new();
5369    for ordinary in [&plan.overapprox, &plan.lower, &plan.upper] {
5370        for (name, rel_id) in &ordinary.rel_ids {
5371            rel_ids.insert(name.clone(), *rel_id);
5372        }
5373    }
5374    rel_ids
5375}
5376
5377fn schema_from_pred_decl(
5378    decl: &xlog_logic::ast::PredDecl,
5379    domains: &HashMap<String, ScalarType>,
5380) -> Result<Schema> {
5381    let columns = decl.schema_columns();
5382    let resolved = columns
5383        .iter()
5384        .enumerate()
5385        .map(|(idx, column)| {
5386            let name = column.name.clone().unwrap_or_else(|| format!("c{idx}"));
5387            resolve_pred_column_type(&decl.name, idx, &column.typ, domains).map(|typ| (name, typ))
5388        })
5389        .collect::<Result<Vec<_>>>()?;
5390    Ok(Schema::new(resolved))
5391}
5392
5393fn resolve_pred_column_type(
5394    predicate: &str,
5395    index: usize,
5396    typ: &TypeRef,
5397    domains: &HashMap<String, ScalarType>,
5398) -> Result<ScalarType> {
5399    match typ {
5400        TypeRef::Scalar(ty) => Ok(*ty),
5401        TypeRef::Domain(name) => domains.get(name).copied().ok_or_else(|| {
5402            XlogError::Compilation(format!(
5403                "unknown domain alias '{}' in predicate '{}' column {}",
5404                name, predicate, index
5405            ))
5406        }),
5407        TypeRef::List(_) | TypeRef::Term | TypeRef::Compound | TypeRef::PredRef => {
5408            Ok(ScalarType::U64)
5409        }
5410    }
5411}
5412
5413fn schema_from_terms(terms: &[Term]) -> Schema {
5414    let columns = terms
5415        .iter()
5416        .enumerate()
5417        .map(|(idx, term)| (format!("c{idx}"), term.inferred_scalar_type()))
5418        .collect();
5419    Schema::new(columns)
5420}
5421
5422/// Desugar a shared-variable epistemic constraint — a constraint with at least one
5423/// epistemic literal and a variable appearing in more than one term position across the body
5424/// (the join `:- know p(X), possible q(X).`, the diagonal `:- know p(X, X).`, or the
5425/// negated-difference `:- q(X), not know p(X).`) — into an ordinary extraction rule plus a
5426/// single-occurrence modal over it:
5427///
5428/// ```text
5429///   :- BodyLit1, BodyLit2, ..., BodyLitN.
5430///        ==> __epi_join_N(Vars) :- ord(BodyLit1), ..., ord(BodyLitN).
5431///            :- know __epi_join_N(Vars).
5432/// ```
5433///
5434/// where `ord` ordinary-izes each modal literal (`know/possible r(..)` -> `r(..)`,
5435/// `not know/possible r(..)` -> `not r(..)`) and keeps non-modal literals unchanged. For a
5436/// base/EDB or purely-ordinary-derived modal target `know r == possible r == r`, so the
5437/// ordinary join `__epi_join_N` is exactly the set of variable bindings the constraint
5438/// forbids; the single-occurrence `:- know __epi_join_N(Vars)` then routes through the
5439/// existing variable-keyed world-view constraint path, which prunes the world view to empty —
5440/// with NO new kernel. Applied at the normalization choke point so BOTH the reduced ordinary
5441/// materialization and the epistemic planner observe the helper relation (an EIR-only rewrite
5442/// is accepted at planning but the helper is never materialized).
5443///
5444/// Guarded to non-modal-derived targets (where the `know == possible == ordinary`
5445/// equivalence holds); a constraint with a modal-derived target is left unchanged and falls
5446/// through to the core compiler's existing shared-variable rejection. Single-occurrence
5447/// variable-keyed modal, distinct-variable multi-literal, and ground constraints have no
5448/// repeated variable and are likewise untouched.
5449fn desugar_shared_variable_epistemic_constraints(mut program: Program) -> Program {
5450    // A predicate defined by any rule carrying an epistemic body literal is "modal-derived":
5451    // for it `know p`/`possible p` is NOT equal to the ordinary `p`, so ordinary-izing it
5452    // would be UNSOUND. Restrict the desugaring to base/EDB or purely-ordinary-derived
5453    // targets (where `know p == possible p == p`), the case for base tuple-key targets.
5454    let modal_derived: BTreeSet<String> = program
5455        .rules
5456        .iter()
5457        .filter(|rule| {
5458            rule.body
5459                .iter()
5460                .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
5461        })
5462        .map(|rule| rule.head.predicate.clone())
5463        .collect();
5464    let mut extraction_rules: Vec<Rule> = Vec::new();
5465    let mut counter = 0usize;
5466    for constraint in &mut program.constraints {
5467        let has_epistemic = constraint
5468            .body
5469            .iter()
5470            .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
5471        if !has_epistemic || !constraint_has_shared_variable(&constraint.body) {
5472            continue;
5473        }
5474        // Sound only when EVERY modal target is non-modal-derived (know == possible == ord).
5475        let has_modal_derived_target = constraint.body.iter().any(|lit| {
5476            matches!(lit, BodyLiteral::Epistemic(e) if modal_derived.contains(&e.atom.predicate))
5477        });
5478        if has_modal_derived_target {
5479            continue;
5480        }
5481        let distinct = distinct_body_variables(&constraint.body);
5482        let helper = format!("__epi_join_{counter}");
5483        counter += 1;
5484        let helper_terms: Vec<Term> = distinct.iter().map(|v| Term::Variable(v.clone())).collect();
5485        let helper_body: Vec<BodyLiteral> = constraint
5486            .body
5487            .iter()
5488            .map(ordinaryize_modal_literal)
5489            .collect();
5490        extraction_rules.push(Rule {
5491            head: Atom {
5492                predicate: helper.clone(),
5493                terms: helper_terms.clone(),
5494            },
5495            body: helper_body,
5496        });
5497        // Replace the whole constraint with a single-occurrence modal over the join helper.
5498        constraint.body = vec![BodyLiteral::Epistemic(EpistemicLiteral {
5499            op: EpistemicOp::Know,
5500            negated: false,
5501            atom: Atom {
5502                predicate: helper,
5503                terms: helper_terms,
5504            },
5505        })];
5506    }
5507    program.rules.extend(extraction_rules);
5508    program
5509}
5510
5511/// Replace a modal literal with its ordinary counterpart (`know/possible r` -> `r`,
5512/// `not know/possible r` -> `not r`); non-modal literals are returned unchanged. Sound for
5513/// the shared-variable constraint desugaring when the modal target is non-modal-derived,
5514/// where `know r == possible r == r`.
5515fn ordinaryize_modal_literal(lit: &BodyLiteral) -> BodyLiteral {
5516    match lit {
5517        BodyLiteral::Epistemic(e) if e.negated => BodyLiteral::Negated(e.atom.clone()),
5518        BodyLiteral::Epistemic(e) => BodyLiteral::Positive(e.atom.clone()),
5519        other => other.clone(),
5520    }
5521}
5522
5523/// True if some variable occurs in more than one atom term position across the constraint
5524/// body — the signature of a join / diagonal / negated-difference the core compiler rejects.
5525fn constraint_has_shared_variable(body: &[BodyLiteral]) -> bool {
5526    let mut counts: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
5527    for lit in body {
5528        if let Some(atom) = lit.atom() {
5529            for term in &atom.terms {
5530                if let Term::Variable(name) = term {
5531                    *counts.entry(name.clone()).or_insert(0) += 1;
5532                }
5533            }
5534        }
5535    }
5536    counts.values().any(|&count| count > 1)
5537}
5538
5539/// Ordered DISTINCT variable names appearing in atom positions across the constraint body
5540/// (first-appearance order), used as the extracted helper relation's columns.
5541fn distinct_body_variables(body: &[BodyLiteral]) -> Vec<String> {
5542    let mut seen = BTreeSet::new();
5543    let mut order = Vec::new();
5544    for lit in body {
5545        if let Some(atom) = lit.atom() {
5546            for term in &atom.terms {
5547                if let Term::Variable(name) = term {
5548                    if seen.insert(name.clone()) {
5549                        order.push(name.clone());
5550                    }
5551                }
5552            }
5553        }
5554    }
5555    order
5556}
5557
5558fn augment_same_name_multi_arity_schemas(
5559    program: &Program,
5560    schemas: &mut HashMap<String, Schema>,
5561) -> Result<()> {
5562    let predicates = if program_has_epistemic_literals(program) {
5563        epistemic_extensional_multi_arity_predicates(program)
5564    } else {
5565        predicate_arities(program)
5566            .into_iter()
5567            .filter_map(|(predicate, arities)| (arities.len() > 1).then_some(predicate))
5568            .collect()
5569    };
5570    let domains: HashMap<String, ScalarType> = program
5571        .domains
5572        .iter()
5573        .map(|domain| (domain.name.clone(), domain.typ))
5574        .collect();
5575
5576    for decl in &program.predicates {
5577        if !predicates.contains(&decl.name) {
5578            continue;
5579        }
5580        let key = arity_qualified_name(&decl.name, decl.arity());
5581        schemas.insert(key, schema_from_pred_decl(decl, &domains)?);
5582    }
5583
5584    for fact in program.facts() {
5585        let pred = fact.head.predicate.as_str();
5586        let arity = fact.head.terms.len();
5587        if !predicates.contains(pred) {
5588            continue;
5589        }
5590        let key = arity_qualified_name(pred, arity);
5591        schemas
5592            .entry(key)
5593            .or_insert_with(|| schema_from_terms(&fact.head.terms));
5594    }
5595
5596    for rule in &program.rules {
5597        augment_atom_schema_if_needed(&rule.head, &predicates, schemas);
5598        for literal in &rule.body {
5599            match literal {
5600                BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
5601                    augment_atom_schema_if_needed(atom, &predicates, schemas);
5602                }
5603                BodyLiteral::Epistemic(epistemic) => {
5604                    augment_atom_schema_if_needed(&epistemic.atom, &predicates, schemas);
5605                }
5606                BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
5607            }
5608        }
5609    }
5610
5611    for query in &program.queries {
5612        augment_atom_schema_if_needed(&query.atom, &predicates, schemas);
5613    }
5614
5615    Ok(())
5616}
5617
5618/// Arity-qualify same-name multi-arity predicates; `None` when the program has
5619/// no such overloads (the caller then compiles `program` itself, unchanged).
5620fn qualify_same_name_multi_arity_program(program: &Program) -> Option<Program> {
5621    let overloaded = predicate_arities(program)
5622        .into_iter()
5623        .filter_map(|(predicate, arities)| (arities.len() > 1).then_some(predicate))
5624        .collect::<BTreeSet<_>>();
5625    if overloaded.is_empty() {
5626        return None;
5627    }
5628
5629    let mut qualified = program.clone();
5630    for declaration in &mut qualified.predicates {
5631        if overloaded.contains(&declaration.name) {
5632            declaration.name = arity_qualified_name(&declaration.name, declaration.arity());
5633        }
5634    }
5635    for rule in &mut qualified.rules {
5636        qualify_atom_arity(&mut rule.head, &overloaded);
5637        qualify_body_literal_arities(&mut rule.body, &overloaded);
5638    }
5639    for constraint in &mut qualified.constraints {
5640        qualify_body_literal_arities(&mut constraint.body, &overloaded);
5641    }
5642    for query in &mut qualified.queries {
5643        qualify_atom_arity(&mut query.atom, &overloaded);
5644    }
5645    Some(qualified)
5646}
5647
5648fn qualify_body_literal_arities(literals: &mut [BodyLiteral], overloaded: &BTreeSet<String>) {
5649    for literal in literals {
5650        match literal {
5651            BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
5652                qualify_atom_arity(atom, overloaded);
5653            }
5654            BodyLiteral::Epistemic(epistemic) => {
5655                qualify_atom_arity(&mut epistemic.atom, overloaded);
5656            }
5657            BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
5658        }
5659    }
5660}
5661
5662fn qualify_atom_arity(atom: &mut Atom, overloaded: &BTreeSet<String>) {
5663    if overloaded.contains(&atom.predicate) {
5664        atom.predicate = arity_qualified_name(&atom.predicate, atom.terms.len());
5665    }
5666}
5667
5668fn augment_atom_schema_if_needed(
5669    atom: &Atom,
5670    predicates: &BTreeSet<String>,
5671    schemas: &mut HashMap<String, Schema>,
5672) {
5673    if !predicates.contains(&atom.predicate) {
5674        return;
5675    }
5676    let key = arity_qualified_name(&atom.predicate, atom.terms.len());
5677    schemas
5678        .entry(key)
5679        .or_insert_with(|| schema_from_terms(&atom.terms));
5680}
5681
5682fn predicate_arities(program: &Program) -> HashMap<String, BTreeSet<usize>> {
5683    let mut arities = HashMap::new();
5684    for decl in &program.predicates {
5685        add_predicate_arity(&mut arities, &decl.name, decl.arity());
5686    }
5687    for rule in &program.rules {
5688        add_predicate_arity(&mut arities, &rule.head.predicate, rule.head.terms.len());
5689        for literal in &rule.body {
5690            match literal {
5691                BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
5692                    add_predicate_arity(&mut arities, &atom.predicate, atom.terms.len());
5693                }
5694                BodyLiteral::Epistemic(epistemic) => {
5695                    add_predicate_arity(
5696                        &mut arities,
5697                        &epistemic.atom.predicate,
5698                        epistemic.atom.terms.len(),
5699                    );
5700                }
5701                BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
5702            }
5703        }
5704    }
5705    for query in &program.queries {
5706        add_predicate_arity(&mut arities, &query.atom.predicate, query.atom.terms.len());
5707    }
5708    for constraint in &program.constraints {
5709        for literal in &constraint.body {
5710            match literal {
5711                BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
5712                    add_predicate_arity(&mut arities, &atom.predicate, atom.terms.len());
5713                }
5714                BodyLiteral::Epistemic(epistemic) => {
5715                    add_predicate_arity(
5716                        &mut arities,
5717                        &epistemic.atom.predicate,
5718                        epistemic.atom.terms.len(),
5719                    );
5720                }
5721                BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
5722            }
5723        }
5724    }
5725    arities
5726}
5727
5728fn add_predicate_arity(
5729    arities: &mut HashMap<String, BTreeSet<usize>>,
5730    predicate: &str,
5731    arity: usize,
5732) {
5733    arities
5734        .entry(predicate.to_string())
5735        .or_default()
5736        .insert(arity);
5737}
5738
5739fn arity_qualified_name(predicate: &str, arity: usize) -> String {
5740    format!("{predicate}/{arity}")
5741}
5742
5743fn program_has_epistemic_literals(program: &Program) -> bool {
5744    program.rules.iter().any(|rule| {
5745        rule.body
5746            .iter()
5747            .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
5748    }) || program.constraints.iter().any(|constraint| {
5749        constraint
5750            .body
5751            .iter()
5752            .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
5753    })
5754}
5755
5756fn epistemic_output_head_predicate_count(program: &Program) -> usize {
5757    program
5758        .rules
5759        .iter()
5760        .filter(|rule| {
5761            rule.body
5762                .iter()
5763                .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
5764        })
5765        .map(|rule| rule.head.predicate.as_str())
5766        .collect::<BTreeSet<_>>()
5767        .len()
5768}
5769
5770/// The user-visible output head predicate(s) of a stratum's epistemic-bearing
5771/// rules. For a recursive stratum (`reach :- reach, know a`) this is the recursive
5772/// head whose materialized relation is the stratum's output.
5773fn epistemic_stratum_output_heads(program: &Program) -> Vec<String> {
5774    program
5775        .rules
5776        .iter()
5777        .filter(|rule| {
5778            rule.body
5779                .iter()
5780                .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
5781        })
5782        .map(|rule| rule.head.predicate.clone())
5783        .collect::<BTreeSet<_>>()
5784        .into_iter()
5785        .collect()
5786}
5787
5788fn epistemic_relation_ids(plan: &LogicExecutionPlan) -> Result<HashMap<String, RelId>> {
5789    let mut rel_ids = HashMap::new();
5790    match plan {
5791        LogicExecutionPlan::EpistemicSingle(executable) => {
5792            for (name, rel_id) in &executable.relation_ids {
5793                insert_epistemic_relation_id(&mut rel_ids, name, *rel_id)?;
5794            }
5795        }
5796        LogicExecutionPlan::EpistemicSplit(split) => {
5797            for component in &split.components {
5798                for (name, rel_id) in &component.executable.relation_ids {
5799                    insert_epistemic_relation_id(&mut rel_ids, name, *rel_id)?;
5800                }
5801            }
5802        }
5803        LogicExecutionPlan::EpistemicStratified(stratified) => {
5804            for stratum in &stratified.strata {
5805                match &stratum.plan {
5806                    StratumPlanKind::Single(executable) => {
5807                        for (name, rel_id) in &executable.relation_ids {
5808                            // Each stratum is a distinct sub-program compiled with a
5809                            // fresh compiler, so relation ids legitimately differ
5810                            // across strata; keep the last writer per name.
5811                            rel_ids.insert(name.clone(), *rel_id);
5812                        }
5813                    }
5814                    StratumPlanKind::Split(split) => {
5815                        for component in &split.components {
5816                            for (name, rel_id) in &component.executable.relation_ids {
5817                                rel_ids.insert(name.clone(), *rel_id);
5818                            }
5819                        }
5820                    }
5821                    // An ordinary (Case-A recursive) stratum carries no epistemic
5822                    // relation-id map; its relations are owned by its own ordinary
5823                    // RIR plan and surfaced from the store after execution.
5824                    StratumPlanKind::Ordinary { .. } => {}
5825                }
5826            }
5827        }
5828        LogicExecutionPlan::EpistemicWfsGpu(wfs) => {
5829            for plan in [&wfs.overapprox, &wfs.lower, &wfs.upper] {
5830                for (name, rel_id) in &plan.rel_ids {
5831                    rel_ids.insert(name.clone(), *rel_id);
5832                }
5833            }
5834        }
5835        LogicExecutionPlan::EpistemicG91Compatibility(g91) => {
5836            for pass in [&g91.upper_bound, &g91.refinement] {
5837                for (name, rel_id) in gpu_evaluation_pass_rel_ids(pass) {
5838                    rel_ids.insert(name, rel_id);
5839                }
5840            }
5841        }
5842        LogicExecutionPlan::Ordinary(_) => {}
5843    }
5844    Ok(rel_ids)
5845}
5846
5847fn insert_epistemic_relation_id(
5848    rel_ids: &mut HashMap<String, RelId>,
5849    name: &str,
5850    rel_id: RelId,
5851) -> Result<()> {
5852    if let Some(previous) = rel_ids.insert(name.to_string(), rel_id) {
5853        if previous != rel_id {
5854            return Err(XlogError::Compilation(format!(
5855                "epistemic split components assigned conflicting relation ids for {name}: \
5856                 {previous:?} vs {rel_id:?}"
5857            )));
5858        }
5859    }
5860    Ok(())
5861}
5862
5863fn capacities_for_epistemic_executable(
5864    executable: &EpistemicExecutablePlan,
5865) -> Result<EpistemicGpuWorkspaceCapacities> {
5866    let literal_count = executable.gpu_plan.epistemic_literals.len();
5867    let max_candidates = 1usize.checked_shl(literal_count as u32).ok_or_else(|| {
5868        XlogError::UnsupportedEpistemicConstruct {
5869            construct: "epistemic GPU execution candidate generation".to_string(),
5870            context: format!("literal count {literal_count} exceeds target pointer width"),
5871        }
5872    })?;
5873    Ok(EpistemicGpuWorkspaceCapacities {
5874        max_candidates,
5875        max_worlds: 1,
5876        max_models_per_reduction: DEFAULT_EPISTEMIC_MAX_MODELS_PER_REDUCTION,
5877    })
5878}
5879
5880fn capacities_for_epistemic_split(
5881    split: &EpistemicSplitExecutablePlan,
5882) -> Result<EpistemicGpuWorkspaceCapacities> {
5883    let mut capacities = EpistemicGpuWorkspaceCapacities {
5884        max_candidates: 1,
5885        max_worlds: 1,
5886        max_models_per_reduction: DEFAULT_EPISTEMIC_MAX_MODELS_PER_REDUCTION,
5887    };
5888    for component in &split.components {
5889        let component_capacities = capacities_for_epistemic_executable(&component.executable)?;
5890        capacities.max_candidates = capacities
5891            .max_candidates
5892            .max(component_capacities.max_candidates);
5893    }
5894    Ok(capacities)
5895}
5896
5897fn epistemic_output_relation_name(executable: &EpistemicExecutablePlan) -> Result<String> {
5898    executable
5899        .gpu_plan
5900        .reductions
5901        .last()
5902        .map(|reduction| reduction.head_predicate.clone())
5903        .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
5904            construct: "epistemic GPU reduced output".to_string(),
5905            context: "executable plan has no epistemic reductions".to_string(),
5906        })
5907}
5908
5909fn epistemic_buffer_to_query_result(relation_name: String, buffer: CudaBuffer) -> LogicQueryResult {
5910    let schema = buffer.schema();
5911    let columns = schema
5912        .columns
5913        .iter()
5914        .map(|(name, _)| name.clone())
5915        .collect();
5916    let sort_labels = schema.sort_labels().to_vec();
5917    LogicQueryResult {
5918        relation_name,
5919        columns,
5920        sort_labels,
5921        buffer,
5922    }
5923}
5924
5925/// Convert an epistemic GPU execution result into one query result per output head.
5926///
5927/// `primary_relation_name` is the primary head (from `final_output`). A
5928/// JOINT-SOLVED coalesced multi-head component also carries
5929/// `additional_head_outputs`, each materialized against the SAME accepted world
5930/// view; every coupled head becomes its own query result so `xlog run` displays
5931/// all coupled epistemic outputs.
5932fn epistemic_result_to_query_results(
5933    primary_relation_name: String,
5934    result: EpistemicGpuExecutionResult,
5935) -> Vec<LogicQueryResult> {
5936    let mut results = Vec::with_capacity(1 + result.additional_head_outputs.len());
5937    for (head, buffer) in result.additional_head_outputs {
5938        results.push(epistemic_buffer_to_query_result(head, buffer));
5939    }
5940    results.push(epistemic_buffer_to_query_result(
5941        primary_relation_name,
5942        result.final_output,
5943    ));
5944    results
5945}
5946
5947fn is_user_visible_relation(name: &str) -> bool {
5948    !name.starts_with("__")
5949}
5950
5951fn is_list_helper_relation(name: &str) -> bool {
5952    name.starts_with("__xlog_list_")
5953}
5954
5955fn relation_clone_error(context: String, error: XlogError) -> XlogError {
5956    match error {
5957        XlogError::ResourceExhausted {
5958            context: source_context,
5959            estimated_bytes,
5960            budget_bytes,
5961        } => XlogError::ResourceExhausted {
5962            context: format!("{context}: {source_context}"),
5963            estimated_bytes,
5964            budget_bytes,
5965        },
5966        XlogError::Kernel(message) => XlogError::Kernel(format!("{context}: {message}")),
5967        error => error,
5968    }
5969}
5970
5971fn logic_delta_report(
5972    stats: DeltaRecomputeStats,
5973    insert_rows: u64,
5974    delete_rows: u64,
5975) -> LogicDeltaReport {
5976    LogicDeltaReport {
5977        input_delta_count: stats.changed_relations,
5978        changed_relations: stats.changed_relations,
5979        changed_relation_names: Vec::new(),
5980        insert_rows,
5981        delete_rows,
5982        has_deletes: stats.has_deletes,
5983        affected_sccs: stats.affected_sccs,
5984        recomputed_sccs: stats.recomputed_sccs,
5985        incremental_sccs: stats.incremental_sccs,
5986        coalesced_insert_rows: insert_rows,
5987        coalesced_delete_rows: delete_rows,
5988        canceled_rows: 0,
5989        planner_telemetry: DeltaPlannerTelemetry::default(),
5990        debug_trace: Vec::new(),
5991    }
5992}
5993
5994fn no_op_delta_report(seed: PreparedRelationDeltaReportSeed) -> LogicDeltaReport {
5995    LogicDeltaReport {
5996        input_delta_count: seed.input_delta_count,
5997        changed_relations: 0,
5998        changed_relation_names: Vec::new(),
5999        insert_rows: 0,
6000        delete_rows: 0,
6001        has_deletes: false,
6002        affected_sccs: 0,
6003        recomputed_sccs: 0,
6004        incremental_sccs: 0,
6005        coalesced_insert_rows: 0,
6006        coalesced_delete_rows: 0,
6007        canceled_rows: seed.canceled_rows,
6008        planner_telemetry: DeltaPlannerTelemetry {
6009            fallback_decision: "no_op".to_string(),
6010            ..DeltaPlannerTelemetry::default()
6011        },
6012        debug_trace: vec![format!("canceled_rows={}", seed.canceled_rows)],
6013    }
6014}
6015
6016fn delta_debug_trace(report: &LogicDeltaReport) -> Vec<String> {
6017    vec![
6018        format!("changed_relation_names={:?}", report.changed_relation_names),
6019        format!("affected_sccs={}", report.affected_sccs),
6020        format!("recomputed_sccs={}", report.recomputed_sccs),
6021        format!("incremental_sccs={}", report.incremental_sccs),
6022        format!("insert_rows={}", report.insert_rows),
6023        format!("delete_rows={}", report.delete_rows),
6024        format!(
6025            "planner_fallback_decision={}",
6026            report.planner_telemetry.fallback_decision
6027        ),
6028        format!(
6029            "estimated_delta_speedup={:?}",
6030            report.planner_telemetry.estimated_delta_speedup
6031        ),
6032    ]
6033}
6034
6035fn buffers_gpu_set_equivalent(
6036    provider: &CudaKernelProvider,
6037    left: &CudaBuffer,
6038    right: &CudaBuffer,
6039) -> Result<bool> {
6040    if left.schema() != right.schema() {
6041        return Ok(false);
6042    }
6043    let left_rows = provider.device_row_count(left)?;
6044    let right_rows = provider.device_row_count(right)?;
6045    if left_rows != right_rows {
6046        return Ok(false);
6047    }
6048
6049    let left_minus_right = provider.diff_full_row(left, right)?;
6050    if provider.device_row_count(&left_minus_right)? != 0 {
6051        return Ok(false);
6052    }
6053    let right_minus_left = provider.diff_full_row(right, left)?;
6054    Ok(provider.device_row_count(&right_minus_left)? == 0)
6055}
6056
6057fn coalesce_relation_delta_batch_with_cancellation_capture(
6058    provider: &CudaKernelProvider,
6059    delta_batch: Vec<(String, RelationDelta)>,
6060    cancellation_capture_relations: &BTreeSet<String>,
6061) -> Result<PreparedRelationDeltaBatch> {
6062    let input_delta_count = delta_batch.len();
6063    let mut pending_by_relation: HashMap<String, PendingRelationDelta> = HashMap::new();
6064    let mut cancellations: HashMap<String, Vec<RelationDeltaCancellation>> = HashMap::new();
6065    let mut canceled_rows = 0u64;
6066
6067    for (update_index, (name, delta)) in delta_batch.into_iter().enumerate() {
6068        let capture_cancellations = cancellation_capture_relations.contains(&name);
6069        let cancellation_relation = capture_cancellations.then(|| name.clone());
6070        let mut update_cancellations = capture_cancellations.then(Vec::new);
6071        let pending = pending_by_relation.entry(name).or_default();
6072        if let Some(insert) = delta.insert {
6073            merge_insert_delta(
6074                provider,
6075                pending,
6076                insert,
6077                &mut canceled_rows,
6078                update_index,
6079                update_cancellations.as_mut(),
6080            )?;
6081        }
6082        if let Some(delete) = delta.delete {
6083            merge_delete_delta(
6084                provider,
6085                pending,
6086                delete,
6087                &mut canceled_rows,
6088                update_index,
6089                update_cancellations.as_mut(),
6090            )?;
6091        }
6092        if let Some(mut captured) = update_cancellations.filter(|trace| !trace.is_empty()) {
6093            cancellations
6094                .entry(cancellation_relation.expect("capture relation must be retained"))
6095                .or_default()
6096                .append(&mut captured);
6097        }
6098    }
6099
6100    let mut deltas = HashMap::new();
6101    let mut coalesced_insert_rows = 0u64;
6102    let mut coalesced_delete_rows = 0u64;
6103    for (name, pending) in pending_by_relation {
6104        let insert = pending.insert.and_then(non_empty_buffer);
6105        let delete = pending.delete.and_then(non_empty_buffer);
6106        if insert.is_none() && delete.is_none() {
6107            continue;
6108        }
6109        coalesced_insert_rows += insert.as_ref().map(buffer_rows).unwrap_or(0);
6110        coalesced_delete_rows += delete.as_ref().map(buffer_rows).unwrap_or(0);
6111        deltas.insert(name, RelationDelta::new(insert, delete));
6112    }
6113
6114    let changed_relations = deltas.len();
6115    Ok(PreparedRelationDeltaBatch {
6116        deltas,
6117        cancellations,
6118        report_seed: PreparedRelationDeltaReportSeed {
6119            input_delta_count,
6120            changed_relations,
6121            coalesced_insert_rows,
6122            coalesced_delete_rows,
6123            canceled_rows,
6124        },
6125    })
6126}
6127
6128fn merge_insert_delta(
6129    provider: &CudaKernelProvider,
6130    pending: &mut PendingRelationDelta,
6131    insert: CudaBuffer,
6132    canceled_rows: &mut u64,
6133    update_index: usize,
6134    cancellations: Option<&mut Vec<RelationDeltaCancellation>>,
6135) -> Result<()> {
6136    let mut incoming = provider.dedup_full_row(&insert)?;
6137    if let Some(delete) = pending.delete.take().and_then(non_empty_buffer) {
6138        let delete_before = buffer_rows(&delete);
6139        let delete_after = provider.diff_full_row(&delete, &incoming)?;
6140        let insert_after = provider.diff_full_row(&incoming, &delete)?;
6141        *canceled_rows += delete_before.saturating_sub(buffer_rows(&delete_after));
6142        capture_canceled_tuples(
6143            provider,
6144            &incoming,
6145            &insert_after,
6146            update_index,
6147            RelationDeltaDirection::Insert,
6148            cancellations,
6149        )?;
6150        pending.delete = non_empty_buffer(delete_after);
6151        incoming = insert_after;
6152    }
6153    pending.insert = merge_optional_buffer(provider, pending.insert.take(), incoming)?;
6154    Ok(())
6155}
6156
6157fn merge_delete_delta(
6158    provider: &CudaKernelProvider,
6159    pending: &mut PendingRelationDelta,
6160    delete: CudaBuffer,
6161    canceled_rows: &mut u64,
6162    update_index: usize,
6163    cancellations: Option<&mut Vec<RelationDeltaCancellation>>,
6164) -> Result<()> {
6165    let mut incoming = provider.dedup_full_row(&delete)?;
6166    if let Some(insert) = pending.insert.take().and_then(non_empty_buffer) {
6167        let insert_before = buffer_rows(&insert);
6168        let insert_after = provider.diff_full_row(&insert, &incoming)?;
6169        let delete_after = provider.diff_full_row(&incoming, &insert)?;
6170        *canceled_rows += insert_before.saturating_sub(buffer_rows(&insert_after));
6171        capture_canceled_tuples(
6172            provider,
6173            &incoming,
6174            &delete_after,
6175            update_index,
6176            RelationDeltaDirection::Delete,
6177            cancellations,
6178        )?;
6179        pending.insert = non_empty_buffer(insert_after);
6180        incoming = delete_after;
6181    }
6182    pending.delete = merge_optional_buffer(provider, pending.delete.take(), incoming)?;
6183    Ok(())
6184}
6185
6186fn capture_canceled_tuples(
6187    provider: &CudaKernelProvider,
6188    incoming: &CudaBuffer,
6189    incoming_after_cancellation: &CudaBuffer,
6190    update_index: usize,
6191    incoming_direction: RelationDeltaDirection,
6192    cancellations: Option<&mut Vec<RelationDeltaCancellation>>,
6193) -> Result<()> {
6194    let Some(cancellations) = cancellations else {
6195        return Ok(());
6196    };
6197    let intersection = provider.diff_full_row(incoming, incoming_after_cancellation)?;
6198    if let Some(tuples) = non_empty_buffer(intersection) {
6199        cancellations.push(RelationDeltaCancellation {
6200            update_index,
6201            incoming_direction,
6202            tuples,
6203        });
6204    }
6205    Ok(())
6206}
6207
6208fn merge_optional_buffer(
6209    provider: &CudaKernelProvider,
6210    existing: Option<CudaBuffer>,
6211    incoming: CudaBuffer,
6212) -> Result<Option<CudaBuffer>> {
6213    let Some(incoming) = non_empty_buffer(incoming) else {
6214        return Ok(existing.and_then(non_empty_buffer));
6215    };
6216    match existing.and_then(non_empty_buffer) {
6217        Some(existing) => provider
6218            .union_gpu(&existing, &incoming)
6219            .map(non_empty_buffer),
6220        None => Ok(Some(incoming)),
6221    }
6222}
6223
6224fn non_empty_buffer(buffer: CudaBuffer) -> Option<CudaBuffer> {
6225    if buffer.cached_row_count() == Some(0) || buffer.is_empty() {
6226        None
6227    } else {
6228        Some(buffer)
6229    }
6230}
6231
6232fn buffer_rows(buffer: &CudaBuffer) -> u64 {
6233    buffer
6234        .cached_row_count()
6235        .map(u64::from)
6236        .unwrap_or_else(|| buffer.num_rows())
6237}
6238
6239fn ensure_schema_type_compatible(expected: &Schema, actual: &Schema) -> Result<()> {
6240    if expected.arity() != actual.arity() {
6241        return Err(XlogError::Execution(format!(
6242            "Expected {} columns, got {}",
6243            expected.arity(),
6244            actual.arity()
6245        )));
6246    }
6247    for i in 0..expected.arity() {
6248        let exp = expected.column_type(i).ok_or_else(|| {
6249            XlogError::Execution(format!("Missing expected type for column {}", i))
6250        })?;
6251        let act = actual
6252            .column_type(i)
6253            .ok_or_else(|| XlogError::Execution(format!("Missing actual type for column {}", i)))?;
6254        if exp != act {
6255            return Err(XlogError::Execution(format!(
6256                "Column {} type mismatch: expected {:?}, got {:?}",
6257                i, exp, act
6258            )));
6259        }
6260    }
6261    Ok(())
6262}
6263
6264fn query_output_vars(Query { atom }: &Query) -> Vec<String> {
6265    let mut out = Vec::new();
6266    let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
6267    for term in &atom.terms {
6268        for name in term.variables() {
6269            if seen.insert(name) {
6270                out.push(name.to_string());
6271            }
6272        }
6273    }
6274    out
6275}
6276
6277// --------------------------------------------------------------------------- //
6278// Epistemic-plan / EIR JSON dump
6279// --------------------------------------------------------------------------- //
6280
6281fn json_escape(s: &str) -> String {
6282    let mut out = String::with_capacity(s.len() + 2);
6283    for c in s.chars() {
6284        match c {
6285            '"' => out.push_str("\\\""),
6286            '\\' => out.push_str("\\\\"),
6287            '\n' => out.push_str("\\n"),
6288            '\r' => out.push_str("\\r"),
6289            '\t' => out.push_str("\\t"),
6290            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
6291            c => out.push(c),
6292        }
6293    }
6294    out
6295}
6296
6297/// Deterministic 64-bit FNV-1a hash of a string (stable across runs/processes,
6298/// unlike `std::hash::DefaultHasher` which is randomized). Used as the stable
6299/// epistemic plan id so two dumps of the same plan compare equal.
6300fn fnv1a_64(s: &str) -> u64 {
6301    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
6302    for b in s.as_bytes() {
6303        hash ^= *b as u64;
6304        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
6305    }
6306    hash
6307}
6308
6309/// Extract every `know`/`possible` literal (with negation) from a program's EIR.
6310/// Used to retain epistemic provenance when a Case-A recursive reduction lowers the
6311/// program to an ordinary executable plan.
6312fn collect_eir_epistemic_literals(program: &Program) -> Vec<xlog_ir::EirEpistemicLiteral> {
6313    let mut lits = Vec::new();
6314    if let Ok(eir) = xlog_logic::build_eir(program) {
6315        for rule in &eir.rules {
6316            for lit in &rule.body {
6317                if let xlog_ir::EirBodyLiteral::Epistemic(e) = lit {
6318                    lits.push(e.clone());
6319                }
6320            }
6321        }
6322    }
6323    lits
6324}
6325
6326/// JSON summary for an epistemic source that reduced to a high-level recursive
6327/// execution plan without single-pass epistemic GPU candidate units. Case-A/B
6328/// stratified reductions use the ordinary semi-naive engine; cyclic negated-modal
6329/// reductions use the GPU-backed WFS alternating-fixpoint plan. In both cases the
6330/// modal literals and the fail-closed GPU execution policy are recorded.
6331fn epistemic_provenance_summary_json(
6332    plan_kind: &str,
6333    prov: &EpistemicProvenance,
6334    max_iterations: Option<usize>,
6335    wfs: Option<&EpistemicWfsGpuPlan>,
6336) -> String {
6337    let literals = prov
6338        .literals
6339        .iter()
6340        .map(epistemic_literal_json)
6341        .collect::<Vec<_>>()
6342        .join(",");
6343    let wfs_fixed_relations = wfs
6344        .map(wfs_fixed_relations_json)
6345        .unwrap_or_else(|| "null".to_string());
6346    let wfs_convergence_predicates = wfs
6347        .map(wfs_convergence_predicates_json)
6348        .unwrap_or_else(|| "null".to_string());
6349    let wfs_gpu_passes = if wfs.is_some() {
6350        "[\"overapprox\",\"lower\",\"upper\"]"
6351    } else {
6352        "null"
6353    };
6354    let body = format!(
6355        "{{\"plan_kind\":\"{}\",\"reduction\":\"{}\",\
6356\"epistemic_literals\":[{}],\"units\":[],\"max_iterations\":{},\
6357\"wfs_fixed_relations\":{},\"wfs_convergence_predicates\":{},\
6358\"wfs_gpu_passes\":{},\"execution_backend\":\"{}\",\
6359\"fallback_policy\":\"{}\"}}",
6360        json_escape(plan_kind),
6361        json_escape(prov.reduction),
6362        literals,
6363        max_iterations
6364            .map(|value| value.to_string())
6365            .unwrap_or_else(|| "null".to_string()),
6366        wfs_fixed_relations,
6367        wfs_convergence_predicates,
6368        wfs_gpu_passes,
6369        epistemic_execution_backend_json(xlog_ir::EpistemicExecutionBackend::Gpu),
6370        epistemic_fallback_policy_json(xlog_ir::EpistemicFallbackPolicy::RejectUnsupported)
6371    );
6372    let plan_id = fnv1a_64(&body);
6373    format!(
6374        "{{\"plan_id\":\"epi-{:016x}\",\"plan_kind\":\"{}\",\
6375\"reduction\":\"{}\",\"epistemic_literals\":[{}],\"units\":[],\
6376\"max_iterations\":{},\"wfs_fixed_relations\":{},\
6377\"wfs_convergence_predicates\":{},\"wfs_gpu_passes\":{},\"execution_backend\":\"{}\",\
6378\"fallback_policy\":\"{}\"}}",
6379        plan_id,
6380        json_escape(plan_kind),
6381        json_escape(prov.reduction),
6382        literals,
6383        max_iterations
6384            .map(|value| value.to_string())
6385            .unwrap_or_else(|| "null".to_string()),
6386        wfs_fixed_relations,
6387        wfs_convergence_predicates,
6388        wfs_gpu_passes,
6389        epistemic_execution_backend_json(xlog_ir::EpistemicExecutionBackend::Gpu),
6390        epistemic_fallback_policy_json(xlog_ir::EpistemicFallbackPolicy::RejectUnsupported)
6391    )
6392}
6393
6394fn g91_compatibility_summary_json(
6395    plan_kind: &str,
6396    provenance: &EpistemicProvenance,
6397    plan: &EpistemicG91CompatibilityGpuPlan,
6398) -> String {
6399    let literals = provenance
6400        .literals
6401        .iter()
6402        .map(epistemic_literal_json)
6403        .collect::<Vec<_>>()
6404        .join(",");
6405    let snapshots = plan
6406        .snapshot_relations
6407        .iter()
6408        .map(|(source, snapshot)| {
6409            format!("\"{}\":\"{}\"", json_escape(source), json_escape(snapshot))
6410        })
6411        .collect::<Vec<_>>()
6412        .join(",");
6413    let convergence = plan
6414        .convergence_predicates
6415        .iter()
6416        .map(|predicate| format!("\"{}\"", json_escape(predicate)))
6417        .collect::<Vec<_>>()
6418        .join(",");
6419    let body = format!(
6420        "{{\"plan_kind\":\"{}\",\"reduction\":\"{}\",\
6421\"epistemic_literals\":[{}],\"units\":[],\"max_iterations\":{},\
6422\"snapshot_relations\":{{{}}},\"convergence_predicates\":[{}],\
6423\"gpu_passes\":[\"upper_bound\",\"refinement\"],\
6424\"execution_backend\":\"{}\",\"fallback_policy\":\"{}\"}}",
6425        json_escape(plan_kind),
6426        json_escape(provenance.reduction),
6427        literals,
6428        plan.max_iterations,
6429        snapshots,
6430        convergence,
6431        epistemic_execution_backend_json(xlog_ir::EpistemicExecutionBackend::Gpu),
6432        epistemic_fallback_policy_json(xlog_ir::EpistemicFallbackPolicy::RejectUnsupported)
6433    );
6434    let plan_id = fnv1a_64(&body);
6435    format!(
6436        "{{\"plan_id\":\"epi-{plan_id:016x}\",\"plan_kind\":\"{}\",\
6437\"reduction\":\"{}\",\"epistemic_literals\":[{}],\"units\":[],\
6438\"max_iterations\":{},\"snapshot_relations\":{{{}}},\
6439\"convergence_predicates\":[{}],\
6440\"gpu_passes\":[\"upper_bound\",\"refinement\"],\
6441\"execution_backend\":\"{}\",\"fallback_policy\":\"{}\"}}",
6442        json_escape(plan_kind),
6443        json_escape(provenance.reduction),
6444        literals,
6445        plan.max_iterations,
6446        snapshots,
6447        convergence,
6448        epistemic_execution_backend_json(xlog_ir::EpistemicExecutionBackend::Gpu),
6449        epistemic_fallback_policy_json(xlog_ir::EpistemicFallbackPolicy::RejectUnsupported)
6450    )
6451}
6452
6453fn wfs_fixed_relations_json(wfs: &EpistemicWfsGpuPlan) -> String {
6454    let mut sources: BTreeSet<&str> = BTreeSet::new();
6455    for source in wfs.upper_fixed_names.keys() {
6456        sources.insert(source.as_str());
6457    }
6458    for source in wfs.lower_fixed_names.keys() {
6459        sources.insert(source.as_str());
6460    }
6461    let entries = sources
6462        .into_iter()
6463        .map(|source| {
6464            let upper = wfs
6465                .upper_fixed_names
6466                .get(source)
6467                .map(String::as_str)
6468                .unwrap_or("");
6469            let lower = wfs
6470                .lower_fixed_names
6471                .get(source)
6472                .map(String::as_str)
6473                .unwrap_or("");
6474            format!(
6475                "\"{}\":{{\"upper\":\"{}\",\"lower\":\"{}\"}}",
6476                json_escape(source),
6477                json_escape(upper),
6478                json_escape(lower)
6479            )
6480        })
6481        .collect::<Vec<_>>()
6482        .join(",");
6483    format!("{{{entries}}}")
6484}
6485
6486fn wfs_convergence_predicates_json(wfs: &EpistemicWfsGpuPlan) -> String {
6487    let entries = wfs
6488        .intensional_predicates
6489        .iter()
6490        .map(|pred| format!("\"{}\"", json_escape(pred)))
6491        .collect::<Vec<_>>()
6492        .join(",");
6493    format!("[{entries}]")
6494}
6495
6496fn epistemic_literal_json(lit: &xlog_ir::EirEpistemicLiteral) -> String {
6497    let op = match lit.op {
6498        xlog_ir::EirEpistemicOp::Know => "know",
6499        xlog_ir::EirEpistemicOp::Possible => "possible",
6500    };
6501    format!(
6502        "{{\"op\":\"{}\",\"negated\":{},\"predicate\":\"{}\",\"arity\":{}}}",
6503        op,
6504        lit.negated,
6505        json_escape(&lit.atom.predicate),
6506        lit.atom.arity
6507    )
6508}
6509
6510fn epistemic_execution_backend_json(backend: xlog_ir::EpistemicExecutionBackend) -> &'static str {
6511    match backend {
6512        xlog_ir::EpistemicExecutionBackend::Gpu => "gpu",
6513    }
6514}
6515
6516fn epistemic_fallback_policy_json(policy: xlog_ir::EpistemicFallbackPolicy) -> &'static str {
6517    match policy {
6518        xlog_ir::EpistemicFallbackPolicy::RejectUnsupported => "reject_unsupported",
6519    }
6520}
6521
6522fn epistemic_gpu_plan_json(plan: &xlog_ir::EpistemicGpuPlan) -> String {
6523    let mode = match plan.mode {
6524        xlog_ir::EirEpistemicMode::G91 => "g91",
6525        xlog_ir::EirEpistemicMode::Faeel => "faeel",
6526    };
6527    let literals = plan
6528        .epistemic_literals
6529        .iter()
6530        .map(epistemic_literal_json)
6531        .collect::<Vec<_>>()
6532        .join(",");
6533    let phases = plan
6534        .required_phases
6535        .iter()
6536        .map(|p| format!("\"{:?}\"", p))
6537        .collect::<Vec<_>>()
6538        .join(",");
6539    let kernels = plan
6540        .required_kernel_phases
6541        .iter()
6542        .map(|p| format!("\"{:?}\"", p))
6543        .collect::<Vec<_>>()
6544        .join(",");
6545    let constraints = plan
6546        .constraints
6547        .iter()
6548        .map(|c| {
6549            let idx = c
6550                .literal_indices
6551                .iter()
6552                .map(|i| i.to_string())
6553                .collect::<Vec<_>>()
6554                .join(",");
6555            format!(
6556                "{{\"constraint_index\":{},\"literal_indices\":[{}]}}",
6557                c.constraint_index, idx
6558            )
6559        })
6560        .collect::<Vec<_>>()
6561        .join(",");
6562    let reductions = plan
6563        .reductions
6564        .iter()
6565        .map(|r| {
6566            format!(
6567                "{{\"rule_index\":{},\"head\":\"{}\",\"public_head_arity\":{},\"relational_body_atoms\":{}}}",
6568                r.rule_index,
6569                json_escape(&r.head_predicate),
6570                r.public_head_arity,
6571                r.relational_body_atoms
6572            )
6573        })
6574        .collect::<Vec<_>>()
6575        .join(",");
6576    format!(
6577        "{{\"mode\":\"{}\",\"epistemic_literals\":[{}],\"required_phases\":[{}],\
6578\"required_kernel_phases\":[{}],\"constraints\":[{}],\"reductions\":[{}],\
6579\"execution_backend\":\"{}\",\"fallback_policy\":\"{}\"}}",
6580        mode,
6581        literals,
6582        phases,
6583        kernels,
6584        constraints,
6585        reductions,
6586        epistemic_execution_backend_json(plan.execution_backend),
6587        epistemic_fallback_policy_json(plan.fallback_policy)
6588    )
6589}
6590
6591fn epistemic_plan_summary_json(
6592    plan_kind: &str,
6593    gpu_plans: &[(String, &xlog_ir::EpistemicGpuPlan)],
6594    has_ordinary_post: bool,
6595) -> String {
6596    let mut units = gpu_plans
6597        .iter()
6598        .map(|(label, plan)| {
6599            format!(
6600                "{{\"unit\":\"{}\",\"plan\":{}}}",
6601                json_escape(label),
6602                epistemic_gpu_plan_json(plan)
6603            )
6604        })
6605        .collect::<Vec<_>>();
6606    if has_ordinary_post {
6607        units.push(
6608            "{\"unit\":\"ordinary_post\",\"stage_kind\":\"ordinary_closure_and_constraints\"}"
6609                .to_string(),
6610        );
6611    }
6612    let units = units.join(",");
6613    // Canonical body (without the id) hashed for the stable plan id.
6614    let body = format!(
6615        "{{\"plan_kind\":\"{}\",\"units\":[{}],\"execution_backend\":\"{}\",\"fallback_policy\":\"{}\"}}",
6616        json_escape(plan_kind),
6617        units,
6618        epistemic_execution_backend_json(xlog_ir::EpistemicExecutionBackend::Gpu),
6619        epistemic_fallback_policy_json(xlog_ir::EpistemicFallbackPolicy::RejectUnsupported)
6620    );
6621    let plan_id = fnv1a_64(&body);
6622    format!(
6623        "{{\"plan_id\":\"epi-{:016x}\",\"plan_kind\":\"{}\",\"units\":[{}],\"execution_backend\":\"{}\",\"fallback_policy\":\"{}\"}}",
6624        plan_id,
6625        json_escape(plan_kind),
6626        units,
6627        epistemic_execution_backend_json(xlog_ir::EpistemicExecutionBackend::Gpu),
6628        epistemic_fallback_policy_json(xlog_ir::EpistemicFallbackPolicy::RejectUnsupported)
6629    )
6630}
6631
6632#[cfg(test)]
6633fn finish_test_provider_setup<T>(provider: Result<T>, require_cuda: bool) -> Option<T> {
6634    match provider {
6635        Ok(provider) => Some(provider),
6636        Err(error) if require_cuda => {
6637            panic!("XLOG_REQUIRE_CUDA=1 but CUDA provider construction failed: {error}")
6638        }
6639        Err(error) => {
6640            eprintln!("Skipping test: no CUDA device available ({error})");
6641            None
6642        }
6643    }
6644}
6645
6646#[cfg(test)]
6647mod tests {
6648    use super::*;
6649    use std::sync::Arc;
6650
6651    #[test]
6652    fn semantic_task_program_compiles_distinct_domains_and_preserves_query_order() {
6653        for source in [
6654            "pred rainfall(u32). rainfall(8). ?- rainfall(8). ?- rainfall(2).",
6655            "pred connected(u32,u32). connected(3,4). ?- connected(3,4). ?- connected(4,3).",
6656        ] {
6657            let observer = SemanticLogicTaskProgram::compile(source.to_owned(), [1, 0, 1]).unwrap();
6658            assert_eq!(observer.source, source);
6659            assert_eq!(observer.query_ordinals, [1, 0, 1]);
6660        }
6661    }
6662
6663    #[test]
6664    fn semantic_task_program_rejects_non_boolean_or_missing_queries() {
6665        let source = "pred rainfall(u32). rainfall(8). ?- rainfall(X). ?- rainfall(8).";
6666        assert!(SemanticLogicTaskProgram::compile(source.to_owned(), [0, 1, 1]).is_err());
6667        assert!(SemanticLogicTaskProgram::compile(source.to_owned(), [1, 2, 1]).is_err());
6668        assert!(
6669            SemanticLogicTaskProgram::compile("invalid program".to_owned(), [0, 1, 0]).is_err()
6670        );
6671    }
6672
6673    use xlog_core::{symbol, MemoryBudget, ScalarType};
6674    use xlog_cuda::cuda_graph::CudaGraphNodeKind;
6675    use xlog_ir::RirNode;
6676    use xlog_runtime::resident_graph::{
6677        ResidentGraphDeclineReason, ResidentGraphRouteCertificate, ResidentGraphSchemaCatalog,
6678        ResidentGraphSelectionKind,
6679    };
6680
6681    fn ground_term_encoding_test_provider() -> Option<Arc<CudaKernelProvider>> {
6682        let provider = (|| -> Result<Arc<CudaKernelProvider>> {
6683            Ok(Arc::new(
6684                xlog_cuda::CudaProviderBuilder::new(0, MemoryBudget::with_limit(256 * 1024 * 1024))
6685                    .build()?,
6686            ))
6687        })();
6688
6689        finish_test_provider_setup(
6690            provider,
6691            std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1"),
6692        )
6693    }
6694
6695    fn pinned_corpus_test_provider() -> Option<Arc<CudaKernelProvider>> {
6696        let provider = (|| -> Result<Arc<CudaKernelProvider>> {
6697            Ok(Arc::new(
6698                xlog_cuda::CudaProviderBuilder::new(
6699                    0,
6700                    MemoryBudget::with_limit(2 * 1024 * 1024 * 1024),
6701                )
6702                .build()?,
6703            ))
6704        })();
6705        finish_test_provider_setup(
6706            provider,
6707            std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1"),
6708        )
6709    }
6710
6711    const PINNED_CORPUS_SHA: &str = "74f2895486737b4caa42229389d309994e7ad3ea";
6712    const RESIDENT_ENV_NAMES: [&str; 3] = [
6713        "XLOG_DISABLE_RESIDENT_RECURSION",
6714        "XLOG_REQUIRE_RESIDENT_RECURSION",
6715        RESIDENT_LATENCY_DIAGNOSTICS_ENV,
6716    ];
6717
6718    fn git_output(corpus: &std::path::Path, args: &[&str]) -> String {
6719        let output = std::process::Command::new("git")
6720            .arg("-C")
6721            .arg(corpus)
6722            .args(args)
6723            .output()
6724            .unwrap_or_else(|error| panic!("git {args:?} failed to start: {error}"));
6725        assert!(
6726            output.status.success(),
6727            "git {args:?} failed: {}",
6728            String::from_utf8_lossy(&output.stderr)
6729        );
6730        String::from_utf8(output.stdout).expect("git output must be UTF-8")
6731    }
6732
6733    fn assert_exact_clean_corpus(corpus: &std::path::Path) {
6734        assert_eq!(
6735            git_output(corpus, &["rev-parse", "HEAD"]).trim(),
6736            PINNED_CORPUS_SHA
6737        );
6738        assert!(
6739            git_output(
6740                corpus,
6741                &["status", "--porcelain=v1", "--untracked-files=all"]
6742            )
6743            .is_empty(),
6744            "pinned corpus must have no tracked or untracked modifications"
6745        );
6746        assert!(
6747            git_output(corpus, &["diff", "--no-ext-diff", "--submodule=diff"]).is_empty(),
6748            "pinned corpus working tree must match HEAD"
6749        );
6750        assert!(
6751            git_output(
6752                corpus,
6753                &["diff", "--cached", "--no-ext-diff", "--submodule=diff"]
6754            )
6755            .is_empty(),
6756            "pinned corpus index must match HEAD"
6757        );
6758        let submodules = git_output(corpus, &["submodule", "status", "--recursive"]);
6759        assert!(
6760            submodules.lines().all(|line| line.starts_with(' ')),
6761            "every recursive submodule must be initialized at its recorded commit: {submodules}"
6762        );
6763        let clean_submodules = std::process::Command::new("git")
6764            .arg("-C")
6765            .arg(corpus)
6766            .args([
6767                "submodule",
6768                "foreach",
6769                "--quiet",
6770                "--recursive",
6771                "test -z \"$(git status --porcelain=v1 --untracked-files=all)\"",
6772            ])
6773            .status()
6774            .expect("recursive submodule cleanliness command must start");
6775        assert!(
6776            clean_submodules.success(),
6777            "recursive submodule checkout is dirty"
6778        );
6779    }
6780
6781    #[test]
6782    #[ignore = "requires authorized CUDA execution"]
6783    fn logic_observer_derives_task_truth_from_native_query_execution() -> Result<()> {
6784        let provider = ground_term_encoding_test_provider().ok_or_else(|| {
6785            XlogError::Execution("authorized CUDA observer test requires a working provider".into())
6786        })?;
6787        let source = "pred connected(u32,u32). pred reachable(u32,u32). connected(3,4). reachable(X,Y) :- connected(X,Y). ?- reachable(3,4). ?- reachable(4,3).";
6788        let forward = SemanticLogicTaskProgram::compile(source.to_owned(), [0, 1, 1])?;
6789        let reversed = SemanticLogicTaskProgram::compile(source.to_owned(), [1, 0, 0])?;
6790        let observed = xlog_cuda::SemanticTaskProgram::observe(&forward, Arc::clone(&provider))
6791            .map_err(|error| XlogError::Execution(error.to_string()))?;
6792        let reordered = xlog_cuda::SemanticTaskProgram::observe(&reversed, provider)
6793            .map_err(|error| XlogError::Execution(error.to_string()))?;
6794        assert_eq!(
6795            observed.expected_truth,
6796            [
6797                xlog_cuda::SemanticTruth::True,
6798                xlog_cuda::SemanticTruth::Neither,
6799                xlog_cuda::SemanticTruth::Neither,
6800            ]
6801        );
6802        assert_eq!(
6803            reordered.expected_truth,
6804            [
6805                xlog_cuda::SemanticTruth::Neither,
6806                xlog_cuda::SemanticTruth::True,
6807                xlog_cuda::SemanticTruth::True,
6808            ]
6809        );
6810        assert_eq!(observed.program_source, source.as_bytes());
6811        assert_ne!(observed.input_bytes, reordered.input_bytes);
6812        assert_ne!(observed.result_bytes, reordered.result_bytes);
6813        Ok(())
6814    }
6815
6816    struct ResidentEnvGuard {
6817        old: Vec<(&'static str, Option<std::ffi::OsString>)>,
6818    }
6819
6820    impl ResidentEnvGuard {
6821        fn set(active: &[(&'static str, &'static str)]) -> Self {
6822            let old = RESIDENT_ENV_NAMES
6823                .into_iter()
6824                .map(|name| (name, std::env::var_os(name)))
6825                .collect();
6826            for name in RESIDENT_ENV_NAMES {
6827                // SAFETY: every test using this helper holds `resident_env_lock`.
6828                unsafe { std::env::remove_var(name) };
6829            }
6830            for (name, value) in active {
6831                // SAFETY: every test using this helper holds `resident_env_lock`.
6832                unsafe { std::env::set_var(name, value) };
6833            }
6834            Self { old }
6835        }
6836    }
6837
6838    impl Drop for ResidentEnvGuard {
6839        fn drop(&mut self) {
6840            for (name, value) in self.old.drain(..) {
6841                match value {
6842                    Some(value) => {
6843                        // SAFETY: every test using this helper holds `resident_env_lock`.
6844                        unsafe { std::env::set_var(name, value) };
6845                    }
6846                    None => {
6847                        // SAFETY: every test using this helper holds `resident_env_lock`.
6848                        unsafe { std::env::remove_var(name) };
6849                    }
6850                }
6851            }
6852        }
6853    }
6854
6855    fn resident_env_lock() -> &'static std::sync::Mutex<()> {
6856        static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
6857        LOCK.get_or_init(|| std::sync::Mutex::new(()))
6858    }
6859
6860    #[test]
6861    fn resident_selection_rejects_malformed_boolean_environment() {
6862        let _lock = resident_env_lock().lock().unwrap();
6863        let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "sometimes")]);
6864        assert!(matches!(
6865            ResidentSelectionMode::from_env(),
6866            Err(XlogError::Configuration { ref name, .. })
6867                if name == "XLOG_REQUIRE_RESIDENT_RECURSION"
6868        ));
6869    }
6870
6871    #[test]
6872    fn resident_selection_defaults_to_auto_and_honors_explicit_policy() {
6873        let _lock = resident_env_lock().lock().unwrap();
6874
6875        let automatic = ResidentEnvGuard::set(&[]);
6876        assert_eq!(
6877            ResidentSelectionMode::from_env().unwrap(),
6878            ResidentSelectionMode::Auto
6879        );
6880        drop(automatic);
6881
6882        let disabled = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
6883        assert_eq!(
6884            ResidentSelectionMode::from_env().unwrap(),
6885            ResidentSelectionMode::Disabled
6886        );
6887        drop(disabled);
6888
6889        let _required = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
6890        assert_eq!(
6891            ResidentSelectionMode::from_env().unwrap(),
6892            ResidentSelectionMode::Require
6893        );
6894    }
6895
6896    #[test]
6897    fn resident_selection_rejects_conflicting_explicit_policy() {
6898        let _lock = resident_env_lock().lock().unwrap();
6899        let _env = ResidentEnvGuard::set(&[
6900            ("XLOG_DISABLE_RESIDENT_RECURSION", "1"),
6901            ("XLOG_REQUIRE_RESIDENT_RECURSION", "1"),
6902        ]);
6903        assert!(matches!(
6904            ResidentSelectionMode::from_env(),
6905            Err(XlogError::Execution(ref message))
6906                if message.contains("mutually exclusive")
6907        ));
6908    }
6909
6910    fn corpus_program(corpus: &std::path::Path) -> Result<LogicProgram> {
6911        let entry = corpus.join("scenarios/acceptance/issue1/q01_blind.xlog");
6912        let source = std::fs::read_to_string(&entry).map_err(|error| {
6913            XlogError::Execution(format!("failed to read {}: {error}", entry.display()))
6914        })?;
6915        let resolver = xlog_logic::compile::load_modules(&entry, vec![corpus.join("programs")])
6916            .map_err(|error| XlogError::Compilation(error.to_string()))?;
6917        LogicProgram::compile_with_resolver(&source, &resolver)
6918    }
6919
6920    fn schema_catalog(program: &LogicProgram) -> ResidentGraphSchemaCatalog {
6921        ResidentGraphSchemaCatalog::from_named_schemas(program.rel_ids.iter().filter_map(
6922            |(name, rel)| {
6923                program
6924                    .schemas
6925                    .get(name)
6926                    .cloned()
6927                    .map(|schema| (name.clone(), *rel, schema))
6928            },
6929        ))
6930    }
6931
6932    struct RouteWalk<'a> {
6933        scc_index: usize,
6934        rule_index: usize,
6935        recursive: bool,
6936        identities: &'a mut BTreeSet<String>,
6937    }
6938
6939    impl RouteWalk<'_> {
6940        fn visit(&mut self, node: &RirNode, path: &str) {
6941            assert!(
6942                self.identities.insert(format!(
6943                    "scc={};rule={};recursive={};path={path}",
6944                    self.scc_index, self.rule_index, self.recursive
6945                )),
6946                "route occurrence paths must be unique"
6947            );
6948            match node {
6949                RirNode::Unit | RirNode::Scan { .. } | RirNode::TensorMaskedJoin { .. } => {}
6950                RirNode::Filter { input, .. }
6951                | RirNode::Project { input, .. }
6952                | RirNode::GroupBy { input, .. }
6953                | RirNode::Distinct { input, .. } => self.visit(input, &format!("{path}/input")),
6954                RirNode::Join { left, right, .. } | RirNode::Diff { left, right } => {
6955                    self.visit(left, &format!("{path}/left"));
6956                    self.visit(right, &format!("{path}/right"));
6957                }
6958                RirNode::ChainJoin {
6959                    left,
6960                    right,
6961                    fallback,
6962                    ..
6963                } => {
6964                    self.visit(left, &format!("{path}/primary/left"));
6965                    self.visit(right, &format!("{path}/primary/right"));
6966                    self.visit(fallback, &format!("{path}/alternative/captured_fallback"));
6967                }
6968                RirNode::Union { inputs } => {
6969                    for (index, input) in inputs.iter().enumerate() {
6970                        self.visit(input, &format!("{path}/input[{index}]"));
6971                    }
6972                }
6973                RirNode::Fixpoint {
6974                    base, recursive, ..
6975                } => {
6976                    self.visit(base, &format!("{path}/base"));
6977                    self.visit(recursive, &format!("{path}/recursive"));
6978                }
6979                RirNode::MultiWayJoin {
6980                    inputs, fallback, ..
6981                } => {
6982                    for (index, input) in inputs.iter().enumerate() {
6983                        self.visit(input, &format!("{path}/primary/input[{index}]"));
6984                    }
6985                    self.visit(fallback, &format!("{path}/alternative/captured_fallback"));
6986                }
6987            }
6988        }
6989    }
6990
6991    fn independent_route_identities(plan: &ExecutionPlan) -> BTreeSet<String> {
6992        let mut identities = BTreeSet::new();
6993        for (scc_index, scc) in plan.sccs.iter().enumerate() {
6994            let rules = plan
6995                .rules_by_scc
6996                .get(scc_index)
6997                .unwrap_or_else(|| panic!("missing rule vector for SCC {scc_index}"));
6998            for (rule_index, rule) in rules.iter().enumerate() {
6999                RouteWalk {
7000                    scc_index,
7001                    rule_index,
7002                    recursive: scc.is_recursive,
7003                    identities: &mut identities,
7004                }
7005                .visit(&rule.body, "primary/root");
7006                let rule_identity = format!(
7007                    "scc={scc_index};rule={rule_index};head={};schema={:#?}",
7008                    rule.head, rule.meta.schema
7009                );
7010                identities.insert(format!("{rule_identity};implicit=rule_result_union"));
7011                identities.insert(format!("{rule_identity};implicit=full_row_dedup"));
7012                if scc.is_recursive {
7013                    identities.insert(format!("{rule_identity};implicit=novel_tuple_difference"));
7014                    identities.insert(format!("{rule_identity};implicit=device_convergence"));
7015                }
7016            }
7017        }
7018        identities
7019    }
7020
7021    fn op_count(stats: &ExecutionStats, name: &str) -> usize {
7022        stats
7023            .strata
7024            .iter()
7025            .flat_map(|stratum| &stratum.ops)
7026            .filter(|op| op.op_name == name)
7027            .count()
7028    }
7029
7030    fn strata_op_profile(stats: &ExecutionStats) -> BTreeMap<String, (usize, u64, u64)> {
7031        let mut profile = BTreeMap::new();
7032        for op in stats.strata.iter().flat_map(|stratum| &stratum.ops) {
7033            let entry = profile.entry(op.op_name.clone()).or_insert((0, 0, 0));
7034            entry.0 += 1;
7035            entry.1 += op.input_rows;
7036            entry.2 += op.output_rows;
7037        }
7038        profile
7039    }
7040
7041    #[derive(Debug, Clone, PartialEq, Eq)]
7042    struct HostQuerySnapshot {
7043        relation_name: String,
7044        columns: Vec<String>,
7045        sort_labels: Vec<String>,
7046        schema: Schema,
7047        rows: Vec<Vec<u64>>,
7048    }
7049
7050    fn snapshot_query_results(
7051        provider: &CudaKernelProvider,
7052        result: &LogicEvalResult,
7053    ) -> Result<Vec<HostQuerySnapshot>> {
7054        result
7055            .queries
7056            .iter()
7057            .map(|query| {
7058                let row_count = provider.device_row_count(&query.buffer)?;
7059                let mut columns = Vec::with_capacity(query.buffer.schema().arity());
7060                for index in 0..query.buffer.schema().arity() {
7061                    let ty = query
7062                        .buffer
7063                        .schema()
7064                        .column_type(index)
7065                        .expect("schema arity checked");
7066                    let values = match ty {
7067                        ScalarType::U32 | ScalarType::Symbol => provider
7068                            .download_column::<u32>(&query.buffer, index)?
7069                            .into_iter()
7070                            .map(u64::from)
7071                            .collect(),
7072                        ScalarType::U64 => provider.download_column::<u64>(&query.buffer, index)?,
7073                        ScalarType::I32 => provider
7074                            .download_column::<i32>(&query.buffer, index)?
7075                            .into_iter()
7076                            .map(|value| value as i64 as u64)
7077                            .collect(),
7078                        ScalarType::I64 => provider
7079                            .download_column::<i64>(&query.buffer, index)?
7080                            .into_iter()
7081                            .map(|value| value as u64)
7082                            .collect(),
7083                        ScalarType::F32 => provider
7084                            .download_column::<f32>(&query.buffer, index)?
7085                            .into_iter()
7086                            .map(|value| u64::from(value.to_bits()))
7087                            .collect(),
7088                        ScalarType::F64 => provider
7089                            .download_column::<f64>(&query.buffer, index)?
7090                            .into_iter()
7091                            .map(f64::to_bits)
7092                            .collect(),
7093                        ScalarType::Bool => provider
7094                            .download_column::<u8>(&query.buffer, index)?
7095                            .into_iter()
7096                            .map(u64::from)
7097                            .collect(),
7098                    };
7099                    if values.len() != row_count {
7100                        return Err(XlogError::Execution(format!(
7101                            "query column {index} has {} rows but metadata reports {row_count}",
7102                            values.len()
7103                        )));
7104                    }
7105                    columns.push(values);
7106                }
7107                let mut rows = (0..row_count)
7108                    .map(|row| columns.iter().map(|column| column[row]).collect::<Vec<_>>())
7109                    .collect::<Vec<_>>();
7110                rows.sort_unstable();
7111                Ok(HostQuerySnapshot {
7112                    relation_name: query.relation_name.clone(),
7113                    columns: query.columns.clone(),
7114                    sort_labels: query.sort_labels.clone(),
7115                    schema: query.buffer.schema().clone(),
7116                    rows,
7117                })
7118            })
7119            .collect()
7120    }
7121
7122    #[test]
7123    #[ignore = "requires a serialized release-mode CUDA acceptance run"]
7124    fn resident_semantic_profile_excludes_noop_recursive_variants() -> Result<()> {
7125        let _env_lock = resident_env_lock().lock().expect("resident env lock");
7126        let Some(provider) = ground_term_encoding_test_provider() else {
7127            return Ok(());
7128        };
7129        let program = LogicProgram::compile(
7130            r#"
7131                pred seed(u32).
7132                pred dead(u32).
7133                pred a(u32).
7134                pred b(u32).
7135
7136                seed(1).
7137                a(X) :- seed(X).
7138                b(X) :- dead(X).
7139                a(X) :- b(X), X = 1.
7140                b(X) :- a(X), X = 1.
7141
7142                ?- a(X).
7143                ?- b(X).
7144            "#,
7145        )?;
7146        let empty_recursive_inputs = || -> Result<HashMap<String, CudaBuffer>> {
7147            Ok(HashMap::from([
7148                (
7149                    "a".to_string(),
7150                    provider.create_empty_buffer(program.schema("a").expect("a schema").clone())?,
7151                ),
7152                (
7153                    "b".to_string(),
7154                    provider.create_empty_buffer(program.schema("b").expect("b schema").clone())?,
7155                ),
7156            ]))
7157        };
7158        let baseline = {
7159            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
7160            program.evaluate_with_options(provider.clone(), empty_recursive_inputs()?, true)?
7161        };
7162        let expected = snapshot_query_results(provider.as_ref(), &baseline)?;
7163        let baseline_stats = baseline.stats.as_ref().expect("baseline profile");
7164        let baseline_scans = op_count(baseline_stats, "scan");
7165        let baseline_filters = op_count(baseline_stats, "filter");
7166        let expected_semantic_scans =
7167            baseline_scans as u64 + baseline_stats.chain_fallback_scan_equivalents;
7168        let expected_semantic_filters =
7169            baseline_filters as u64 + baseline_stats.chain_fallback_filter_equivalents;
7170        drop(baseline);
7171
7172        let resident = {
7173            let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
7174            program.evaluate_with_options(provider.clone(), empty_recursive_inputs()?, true)?
7175        };
7176        assert_eq!(
7177            snapshot_query_results(provider.as_ref(), &resident)?,
7178            expected
7179        );
7180        let resident_stats = resident.stats.as_ref().expect("resident profile");
7181        let graph = resident_stats
7182            .resident_graph
7183            .as_ref()
7184            .expect("resident telemetry");
7185        assert_eq!(graph.semantic_scan_invocations, expected_semantic_scans);
7186        assert_eq!(graph.semantic_filter_invocations, expected_semantic_filters);
7187        assert_eq!(
7188            op_count(resident_stats, "scan") as u64,
7189            graph.device_scan_invocations
7190        );
7191        assert_eq!(
7192            op_count(resident_stats, "filter") as u64,
7193            graph.device_filter_invocations
7194        );
7195        assert!(graph.device_scan_invocations >= graph.semantic_scan_invocations);
7196        assert!(graph.device_filter_invocations >= graph.semantic_filter_invocations);
7197        assert!(
7198            graph.device_scan_invocations > graph.semantic_scan_invocations
7199                || graph.device_filter_invocations > graph.semantic_filter_invocations,
7200            "the witness must schedule at least one empty-delta recursive variant"
7201        );
7202        Ok(())
7203    }
7204
7205    #[test]
7206    fn resident_latency_phase_accounting_reports_only_unmeasured_host_work() {
7207        assert_eq!(resident_latency_unattributed_ns(100, &[10, 20, 30]), 40);
7208        assert_eq!(resident_latency_unattributed_ns(50, &[30, 30]), 0);
7209    }
7210
7211    #[test]
7212    fn resident_certification_cache_is_eager_clone_shared_thread_safe_and_compile_isolated(
7213    ) -> Result<()> {
7214        let source = r#"
7215            pred input(u32).
7216            pred output(u32).
7217            output(X) :- input(X).
7218            ?- output(X).
7219        "#;
7220        let program = LogicProgram::compile(source)?;
7221        assert_eq!(program.resident_certification_initializations(), 1);
7222
7223        let barrier = Arc::new(std::sync::Barrier::new(8));
7224        let certified = std::thread::scope(|scope| {
7225            let handles = (0..8)
7226                .map(|_| {
7227                    let clone = program.clone();
7228                    let barrier = Arc::clone(&barrier);
7229                    scope.spawn(move || {
7230                        barrier.wait();
7231                        clone.resident_certified_plan()
7232                    })
7233                })
7234                .collect::<Vec<_>>();
7235            handles
7236                .into_iter()
7237                .map(|handle| handle.join().expect("resident certification worker"))
7238                .collect::<Result<Vec<_>>>()
7239        })?;
7240        assert_eq!(program.resident_certification_initializations(), 1);
7241        assert!(certified
7242            .iter()
7243            .all(|candidate| Arc::ptr_eq(&certified[0], candidate)));
7244
7245        let outcome_program = LogicProgram::compile(source)?;
7246        assert_eq!(outcome_program.resident_certification_initializations(), 1);
7247        let (seeded, cache_was_warm, initialized_here) =
7248            outcome_program.resident_certified_plan_with_outcome()?;
7249        assert!(cache_was_warm);
7250        assert!(!initialized_here);
7251        let (warm, cache_was_warm, initialized_here) =
7252            outcome_program.resident_certified_plan_with_outcome()?;
7253        assert!(cache_was_warm);
7254        assert!(!initialized_here);
7255        assert!(Arc::ptr_eq(&seeded, &warm));
7256
7257        let fresh = LogicProgram::compile(source)?;
7258        assert_eq!(fresh.resident_certification_initializations(), 1);
7259        let fresh_certified = fresh.resident_certified_plan()?;
7260        assert_eq!(fresh.resident_certification_initializations(), 1);
7261        assert!(!Arc::ptr_eq(&certified[0], &fresh_certified));
7262        Ok(())
7263    }
7264
7265    #[test]
7266    fn resident_certification_retains_only_query_and_constraint_dependencies() -> Result<()> {
7267        let program = LogicProgram::compile(
7268            r#"
7269                pred base(u32).
7270                pred edge(u32, u32).
7271                pred reachable(u32).
7272                pred audited(u32).
7273                pred disconnected_seed(u32).
7274                pred disconnected(u32).
7275
7276                base(1).
7277                edge(1, 2).
7278                reachable(X) :- base(X).
7279                reachable(Y) :- reachable(X), edge(X, Y).
7280                audited(X) :- base(X).
7281                disconnected(X) :- disconnected_seed(X).
7282
7283                :- audited(99).
7284                ?- reachable(X).
7285            "#,
7286        )?;
7287
7288        let full = program.ordinary_plan("resident reachability test")?;
7289        let full_heads = full
7290            .rules_by_scc
7291            .iter()
7292            .flatten()
7293            .map(|rule| rule.head.as_str())
7294            .collect::<std::collections::BTreeSet<_>>();
7295        assert!(full_heads.contains("disconnected"));
7296
7297        let certified = program.resident_certified_plan()?;
7298        let resident = certified.plan();
7299        let resident_heads = resident
7300            .rules_by_scc
7301            .iter()
7302            .flatten()
7303            .map(|rule| rule.head.as_str())
7304            .collect::<std::collections::BTreeSet<_>>();
7305
7306        assert!(resident_heads.contains("reachable"));
7307        assert!(resident_heads.contains("audited"));
7308        assert!(resident_heads.contains("__xlog_constraint_0"));
7309        assert!(resident_heads.contains("__xlog_query_0"));
7310        assert!(!resident_heads.contains("disconnected"));
7311        assert!(resident.rules_by_scc.len() < full.rules_by_scc.len());
7312        assert!(resident
7313            .sccs
7314            .iter()
7315            .enumerate()
7316            .all(|(index, scc)| scc.id == index as u32));
7317        assert!(resident
7318            .strata
7319            .iter()
7320            .flat_map(|stratum| &stratum.sccs)
7321            .all(|scc| (*scc as usize) < resident.sccs.len()));
7322        assert_eq!(resident.generated_query_rules.len(), 1);
7323        let query = &resident.generated_query_rules[0];
7324        assert_eq!(query.query_index, 0);
7325        assert_eq!(
7326            resident.rules_by_scc[query.scc_index][query.rule_index].head,
7327            "__xlog_query_0"
7328        );
7329
7330        let full_heads_after_certification = full
7331            .rules_by_scc
7332            .iter()
7333            .flatten()
7334            .map(|rule| rule.head.as_str())
7335            .collect::<std::collections::BTreeSet<_>>();
7336        assert!(full_heads_after_certification.contains("disconnected"));
7337        Ok(())
7338    }
7339
7340    #[test]
7341    fn resident_dependency_closure_fails_closed_on_missing_duplicate_or_ambiguous_proof(
7342    ) -> Result<()> {
7343        #[derive(Debug, PartialEq, Eq)]
7344        struct ResidentPlanStructure {
7345            sccs: Vec<(u32, Vec<String>)>,
7346            strata: Vec<(u32, Vec<u32>)>,
7347            rule_heads: Vec<Vec<String>>,
7348            generated_query_rules: Vec<(usize, usize, usize)>,
7349        }
7350
7351        fn plan_structure(plan: &ExecutionPlan) -> ResidentPlanStructure {
7352            ResidentPlanStructure {
7353                sccs: plan
7354                    .sccs
7355                    .iter()
7356                    .map(|scc| (scc.id, scc.predicates.clone()))
7357                    .collect(),
7358                strata: plan
7359                    .strata
7360                    .iter()
7361                    .map(|stratum| (stratum.id, stratum.sccs.clone()))
7362                    .collect(),
7363                rule_heads: plan
7364                    .rules_by_scc
7365                    .iter()
7366                    .map(|rules| rules.iter().map(|rule| rule.head.clone()).collect())
7367                    .collect(),
7368                generated_query_rules: plan
7369                    .generated_query_rules
7370                    .iter()
7371                    .map(|query| (query.query_index, query.scc_index, query.rule_index))
7372                    .collect(),
7373            }
7374        }
7375
7376        let program = LogicProgram::compile(
7377            r#"
7378                pred input(u32).
7379                pred output(u32).
7380                input(1).
7381                output(X) :- input(X).
7382                ?- output(X).
7383            "#,
7384        )?;
7385        let full = program.ordinary_plan("resident fail-closed test")?;
7386
7387        let mut missing = full.clone();
7388        missing.generated_query_rules.clear();
7389        assert_eq!(
7390            plan_structure(&program.resident_dependency_closed_plan(&missing)),
7391            plan_structure(&missing)
7392        );
7393
7394        let mut duplicate = full.clone();
7395        duplicate
7396            .generated_query_rules
7397            .push(duplicate.generated_query_rules[0].clone());
7398        assert_eq!(
7399            plan_structure(&program.resident_dependency_closed_plan(&duplicate)),
7400            plan_structure(&duplicate)
7401        );
7402
7403        let mut ambiguous = full.clone();
7404        let duplicated_rule = ambiguous
7405            .rules_by_scc
7406            .iter()
7407            .flatten()
7408            .find(|rule| rule.head == "output")
7409            .expect("output rule")
7410            .clone();
7411        let duplicate_scc = ambiguous.sccs.len() as u32;
7412        ambiguous.sccs.push(xlog_ir::Scc {
7413            id: duplicate_scc,
7414            predicates: vec!["output".into()],
7415            is_recursive: false,
7416        });
7417        ambiguous.rules_by_scc.push(vec![duplicated_rule]);
7418        ambiguous.strata.push(xlog_ir::Stratum {
7419            id: ambiguous.strata.len() as u32,
7420            sccs: vec![duplicate_scc],
7421        });
7422        assert_eq!(
7423            plan_structure(&program.resident_dependency_closed_plan(&ambiguous)),
7424            plan_structure(&ambiguous)
7425        );
7426
7427        let mut missing_nonroot_rel_id = full.clone();
7428        missing_nonroot_rel_id
7429            .rules_by_scc
7430            .iter_mut()
7431            .flatten()
7432            .find(|rule| rule.head == "output")
7433            .expect("output rule")
7434            .head = "missing_nonroot_rel_id".into();
7435        assert!(program
7436            .try_resident_dependency_closed_plan(&missing_nonroot_rel_id)
7437            .is_none());
7438        assert_eq!(
7439            plan_structure(&program.resident_dependency_closed_plan(&missing_nonroot_rel_id)),
7440            plan_structure(&missing_nonroot_rel_id)
7441        );
7442
7443        let no_query = LogicProgram::compile(
7444            r#"
7445                pred input(u32).
7446                pred output(u32).
7447                input(1).
7448                output(X) :- input(X).
7449            "#,
7450        )?;
7451        let no_query_full = no_query.ordinary_plan("resident no-query test")?;
7452        assert_eq!(
7453            plan_structure(&no_query.resident_dependency_closed_plan(no_query_full)),
7454            plan_structure(no_query_full)
7455        );
7456        Ok(())
7457    }
7458
7459    #[test]
7460    fn compile_finalizer_preserves_and_replays_deterministic_certification_errors() -> Result<()> {
7461        let mut program = LogicProgram::compile(
7462            r#"
7463                pred output(u32).
7464                output(7).
7465                ?- output(X).
7466            "#,
7467        )?;
7468        program.reusable_state_identity = Arc::new(LogicProgramIdentity::new());
7469        let first = program
7470            .reusable_state_identity
7471            .get_or_init_resident_certification(|| -> Result<ResidentGraphCertifiedPlan> {
7472                Err(XlogError::Execution(
7473                    "deterministic certification failure".into(),
7474                ))
7475            })
7476            .expect_err("injected certification must fail");
7477        let program = program.finalize_compilation();
7478        let second = program
7479            .resident_certified_plan()
7480            .expect_err("cached certification must fail identically");
7481
7482        assert_eq!(first.to_string(), second.to_string());
7483        assert_eq!(program.resident_certification_initializations(), 1);
7484        Ok(())
7485    }
7486
7487    #[test]
7488    fn resident_certification_cache_is_ordinary_only_and_caches_declines_without_policy(
7489    ) -> Result<()> {
7490        let _env_lock = resident_env_lock().lock().expect("resident env lock");
7491        for policy in [
7492            &[][..],
7493            &[("XLOG_DISABLE_RESIDENT_RECURSION", "1")][..],
7494            &[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")][..],
7495        ] {
7496            let program = {
7497                let _env = ResidentEnvGuard::set(policy);
7498                LogicProgram::compile(
7499                    r#"
7500                        pred input(u32).
7501                        pred output(u32).
7502                        output(X) :- input(X).
7503                        ?- output(X).
7504                    "#,
7505                )?
7506            };
7507            assert_eq!(program.resident_certification_initializations(), 1);
7508        }
7509
7510        let epistemic = LogicProgram::compile(
7511            r#"
7512                pred p(u32). pred q(u32).
7513                p(1). q(X) :- p(X), know p(X). ?- q(X).
7514            "#,
7515        )?;
7516        assert!(!matches!(epistemic.plan, LogicExecutionPlan::Ordinary(_)));
7517        assert!(epistemic.resident_certified_plan().is_err());
7518        assert_eq!(epistemic.resident_certification_initializations(), 0);
7519
7520        let reduced_ordinary = LogicProgram::compile(
7521            r#"
7522                #pragma epistemic_mode = faeel
7523                pred node(u32).
7524                pred seed(u32, u32).
7525                pred trust(u32, u32).
7526                pred reach(u32, u32).
7527                node(1). node(2). node(3).
7528                seed(1, 2).
7529                reach(X, Y) :- seed(X, Y).
7530                reach(X, Z) :- reach(X, Y), trust(Y, Z).
7531                trust(2, 3) :- know reach(1, 2).
7532                trust(3, 1) :- know reach(3, 3).
7533                ?- reach(X, Y).
7534            "#,
7535        )?;
7536        assert!(matches!(
7537            reduced_ordinary.plan,
7538            LogicExecutionPlan::Ordinary(_)
7539        ));
7540        assert_eq!(reduced_ordinary.resident_certification_initializations(), 1);
7541
7542        let unsupported = LogicProgram::compile(
7543            r#"
7544                pred unsupported(f64).
7545                unsupported(7.5).
7546                ?- unsupported(X).
7547            "#,
7548        )?;
7549        let first = unsupported.resident_certified_plan()?;
7550        let second = unsupported.resident_certified_plan()?;
7551        assert!(Arc::ptr_eq(&first, &second));
7552        assert!(!first.certificate().is_supported());
7553        assert_eq!(unsupported.resident_certification_initializations(), 1);
7554        Ok(())
7555    }
7556
7557    fn median_seconds(samples: &mut [f64]) -> f64 {
7558        assert!(!samples.is_empty());
7559        samples.sort_by(f64::total_cmp);
7560        samples[samples.len() / 2]
7561    }
7562
7563    #[test]
7564    #[ignore = "requires the exact external issue corpus checkout and CUDA"]
7565    fn pinned_corpus_prepares_resident_graph_without_launching_it() -> Result<()> {
7566        let _env_lock = resident_env_lock().lock().expect("resident env lock");
7567        let corpus = std::path::PathBuf::from(
7568            std::env::var("XLOG_PINNED_CORPUS_ROOT")
7569                .expect("XLOG_PINNED_CORPUS_ROOT must name the pinned corpus checkout"),
7570        );
7571        assert_exact_clean_corpus(&corpus);
7572        let program = corpus_program(&corpus)?;
7573        let plan = program.ordinary_plan("resident graph preflight")?;
7574        let certificate = ResidentGraphRouteCertificate::inspect(plan, &schema_catalog(&program))?;
7575        assert!(certificate.is_supported(), "{:#?}", certificate.declines());
7576
7577        let Some(provider) = pinned_corpus_test_provider() else {
7578            return Ok(());
7579        };
7580        let resident_provider = Arc::clone(&provider);
7581        let executor =
7582            program.prepare_resident_executor(&resident_provider, HashMap::new(), false, plan)?;
7583        let runtime = resident_provider
7584            .memory()
7585            .runtime()
7586            .expect("canonical provider must own an async runtime");
7587        let graph_before = runtime.conditional_graph_stats();
7588        let allocated_before = resident_provider.memory().allocated_bytes();
7589
7590        let prepared = executor
7591            .prepare_resident_graph(plan, &certificate, ResidentGraphPrepareOptions::default())
7592            .map_err(LogicProgram::resident_execution_error)?;
7593        let report = prepared.preflight_report();
7594        let graph_after = runtime.conditional_graph_stats();
7595        assert_eq!(graph_after.launches, graph_before.launches);
7596        assert_eq!(
7597            graph_after.terminal_synchronizations,
7598            graph_before.terminal_synchronizations
7599        );
7600        assert_eq!(
7601            resident_provider
7602                .memory()
7603                .allocated_bytes()
7604                .saturating_sub(allocated_before),
7605            report.tracked_device_allocation_bytes
7606        );
7607        assert!(report.relation_capacity > 0);
7608        assert_eq!(report.parent_graph_nodes, 5);
7609        assert_eq!(report.conditional_while_nodes, 2);
7610        assert_eq!(
7611            report.parent_graph_node_kinds,
7612            vec![
7613                CudaGraphNodeKind::Kernel,
7614                CudaGraphNodeKind::Conditional,
7615                CudaGraphNodeKind::Kernel,
7616                CudaGraphNodeKind::Conditional,
7617                CudaGraphNodeKind::Kernel,
7618            ]
7619        );
7620        assert_eq!(
7621            report.conditional_body_node_kinds,
7622            vec![
7623                vec![CudaGraphNodeKind::Kernel],
7624                vec![CudaGraphNodeKind::Kernel],
7625            ]
7626        );
7627        assert_eq!(report.conditional_body_kernel_counts, vec![1, 1]);
7628        assert_eq!(report.hierarchical_graph_nodes, 7);
7629        eprintln!(
7630            "resident corpus preflight: capacity={} estimated_bytes={} available_bytes={} tracked_allocated_bytes={} parent_nodes={} conditional_while_nodes={}",
7631            report.relation_capacity,
7632            report.estimated_required_bytes,
7633            report.available_bytes_at_admission,
7634            report.tracked_device_allocation_bytes,
7635            report.parent_graph_nodes,
7636            report.conditional_while_nodes,
7637        );
7638        drop(prepared);
7639        assert_eq!(
7640            runtime.conditional_graph_stats().launches,
7641            graph_before.launches
7642        );
7643        Ok(())
7644    }
7645
7646    #[test]
7647    #[ignore = "requires the exact external issue corpus checkout and CUDA"]
7648    fn pinned_corpus_certifies_and_runs_through_the_resident_production_path() -> Result<()> {
7649        fn fallback_scan_filter_counts(node: &RirNode) -> (usize, usize) {
7650            match node {
7651                RirNode::Unit | RirNode::TensorMaskedJoin { .. } => (0, 0),
7652                RirNode::Scan { .. } => (1, 0),
7653                RirNode::Filter { input, .. } => {
7654                    let (scans, filters) = fallback_scan_filter_counts(input);
7655                    (scans, filters + 1)
7656                }
7657                RirNode::Project { input, .. }
7658                | RirNode::GroupBy { input, .. }
7659                | RirNode::Distinct { input, .. } => fallback_scan_filter_counts(input),
7660                RirNode::Join { left, right, .. } | RirNode::Diff { left, right } => {
7661                    let (left_scans, left_filters) = fallback_scan_filter_counts(left);
7662                    let (right_scans, right_filters) = fallback_scan_filter_counts(right);
7663                    (left_scans + right_scans, left_filters + right_filters)
7664                }
7665                RirNode::ChainJoin { fallback, .. } | RirNode::MultiWayJoin { fallback, .. } => {
7666                    fallback_scan_filter_counts(fallback)
7667                }
7668                RirNode::Union { inputs } => inputs.iter().fold((0, 0), |total, input| {
7669                    let current = fallback_scan_filter_counts(input);
7670                    (total.0 + current.0, total.1 + current.1)
7671                }),
7672                RirNode::Fixpoint {
7673                    base, recursive, ..
7674                } => {
7675                    let (base_scans, base_filters) = fallback_scan_filter_counts(base);
7676                    let (recursive_scans, recursive_filters) =
7677                        fallback_scan_filter_counts(recursive);
7678                    (
7679                        base_scans + recursive_scans,
7680                        base_filters + recursive_filters,
7681                    )
7682                }
7683            }
7684        }
7685
7686        let _env_lock = resident_env_lock().lock().expect("resident env lock");
7687        let corpus = std::path::PathBuf::from(
7688            std::env::var("XLOG_PINNED_CORPUS_ROOT")
7689                .expect("XLOG_PINNED_CORPUS_ROOT must name the pinned corpus checkout"),
7690        );
7691        assert_exact_clean_corpus(&corpus);
7692        let compile_and_certification_started = std::time::Instant::now();
7693        let program = corpus_program(&corpus)?;
7694        let compile_and_certification_seconds =
7695            compile_and_certification_started.elapsed().as_secs_f64();
7696        assert_eq!(
7697            program.resident_certification_initializations(),
7698            1,
7699            "ordinary compilation must eagerly seed one resident certification"
7700        );
7701        let plan = program.ordinary_plan("resident graph capability certificate")?;
7702        assert_eq!(plan.sccs.iter().filter(|scc| scc.is_recursive).count(), 2);
7703        assert_eq!(
7704            plan.sccs.iter().filter(|scc| !scc.is_recursive).count(),
7705            1_751
7706        );
7707        assert_eq!(plan.rules_by_scc.iter().map(Vec::len).sum::<usize>(), 4_559);
7708        let projected_reference_plan = program.resident_certified_plan()?.plan().clone();
7709        assert!(projected_reference_plan.sccs.len() < plan.sccs.len());
7710        assert!(
7711            projected_reference_plan
7712                .rules_by_scc
7713                .iter()
7714                .map(Vec::len)
7715                .sum::<usize>()
7716                < plan.rules_by_scc.iter().map(Vec::len).sum::<usize>()
7717        );
7718        let chain_fallbacks = plan
7719            .rules_by_scc
7720            .iter()
7721            .enumerate()
7722            .flat_map(|(scc_index, rules)| {
7723                rules.iter().filter_map(move |rule| {
7724                    let RirNode::ChainJoin { fallback, .. } = &rule.body else {
7725                        return None;
7726                    };
7727                    let (scans, filters) = fallback_scan_filter_counts(fallback.as_ref());
7728                    Some((
7729                        scc_index,
7730                        plan.sccs[scc_index].is_recursive,
7731                        rule.head.clone(),
7732                        scans,
7733                        filters,
7734                    ))
7735                })
7736            })
7737            .collect::<Vec<_>>();
7738        let projected_chain_fallbacks = projected_reference_plan
7739            .rules_by_scc
7740            .iter()
7741            .enumerate()
7742            .flat_map(|(scc_index, rules)| {
7743                let is_recursive = projected_reference_plan.sccs[scc_index].is_recursive;
7744                rules.iter().filter_map(move |rule| {
7745                    let RirNode::ChainJoin { fallback, .. } = &rule.body else {
7746                        return None;
7747                    };
7748                    let (scans, filters) = fallback_scan_filter_counts(fallback);
7749                    Some((scc_index, is_recursive, rule.head.clone(), scans, filters))
7750                })
7751            })
7752            .collect::<Vec<_>>();
7753        eprintln!(
7754            "chain fallback inventory: routes={} scans={} filters={} details={chain_fallbacks:?}",
7755            chain_fallbacks.len(),
7756            chain_fallbacks.iter().map(|route| route.3).sum::<usize>(),
7757            chain_fallbacks.iter().map(|route| route.4).sum::<usize>()
7758        );
7759
7760        // Prove occurrence completeness independently without duplicating the
7761        // certificate's bounded local-node encoding. `matches_plan` and the
7762        // resident-graph mutation tests verify that semantic binding.
7763        let expected_route_identities = independent_route_identities(plan);
7764        let certificate = ResidentGraphRouteCertificate::inspect(plan, &schema_catalog(&program))?;
7765        assert!(certificate.is_supported(), "{:#?}", certificate.declines());
7766        assert!(certificate.matches_plan(plan)?);
7767        let mut covered_structural_bindings = BTreeSet::new();
7768        let mut covered_physical_route_identities = BTreeSet::new();
7769        for descriptor in certificate.covered_route_descriptors() {
7770            if descriptor.starts_with("plan;") {
7771                covered_structural_bindings.insert(descriptor.clone());
7772            } else if descriptor.starts_with("scc=") {
7773                let identity = descriptor
7774                    .split_once(";node=")
7775                    .map_or(descriptor.as_str(), |(identity, _)| identity);
7776                covered_physical_route_identities.insert(identity.to_owned());
7777            } else {
7778                panic!("unknown resident certificate descriptor class: {descriptor}");
7779            }
7780        }
7781        assert!(!covered_structural_bindings.is_empty());
7782        assert!(!covered_physical_route_identities.is_empty());
7783        assert_eq!(covered_physical_route_identities, expected_route_identities);
7784
7785        let Some(provider) = pinned_corpus_test_provider() else {
7786            return Ok(());
7787        };
7788        let baseline = {
7789            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
7790            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
7791        };
7792        let baseline_snapshot = snapshot_query_results(provider.as_ref(), &baseline)?;
7793        let baseline_stats = baseline.stats.as_ref().expect("baseline profile");
7794        let baseline_scans = op_count(baseline_stats, "scan");
7795        let baseline_filters = op_count(baseline_stats, "filter");
7796        let chain_fallback_scan_equivalents = baseline_stats.chain_fallback_scan_equivalents;
7797        let chain_fallback_filter_equivalents = baseline_stats.chain_fallback_filter_equivalents;
7798        let full_semantic_scans = baseline_scans as u64 + chain_fallback_scan_equivalents;
7799        let full_semantic_filters = baseline_filters as u64 + chain_fallback_filter_equivalents;
7800        assert_eq!(
7801            chain_fallback_scan_equivalents,
7802            chain_fallbacks
7803                .iter()
7804                .map(|route| route.3 as u64)
7805                .sum::<u64>()
7806        );
7807        assert_eq!(
7808            chain_fallback_filter_equivalents,
7809            chain_fallbacks
7810                .iter()
7811                .map(|route| route.4 as u64)
7812                .sum::<u64>()
7813        );
7814        eprintln!(
7815            "full-plan baseline operation profile: physical_scans={baseline_scans} physical_filters={baseline_filters} chain_fallback_scan_equivalents={chain_fallback_scan_equivalents} chain_fallback_filter_equivalents={chain_fallback_filter_equivalents} semantic_scans={full_semantic_scans} semantic_filters={full_semantic_filters} triangle={} four_cycle={} free_join={} factorized_delta={}",
7816            baseline_stats.wcoj_triangle_dispatch_count,
7817            baseline_stats.wcoj_4cycle_dispatch_count,
7818            baseline_stats.free_join_dispatch_count,
7819            baseline_stats.factorized_delta_dispatch_count
7820        );
7821        let mut projected_program = program.clone();
7822        projected_program.plan =
7823            LogicExecutionPlan::Ordinary(Box::new(projected_reference_plan.clone()));
7824        let projected_baseline = {
7825            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
7826            projected_program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
7827        };
7828        assert_eq!(
7829            snapshot_query_results(provider.as_ref(), &projected_baseline)?,
7830            baseline_snapshot,
7831            "dependency-closed ordinary reference changed full-plan query semantics"
7832        );
7833        let projected_stats = projected_baseline
7834            .stats
7835            .as_ref()
7836            .expect("dependency-closed ordinary reference profile");
7837        let projected_scans = op_count(projected_stats, "scan");
7838        let projected_filters = op_count(projected_stats, "filter");
7839        assert_eq!(
7840            projected_stats.chain_fallback_scan_equivalents,
7841            projected_chain_fallbacks
7842                .iter()
7843                .map(|route| route.3 as u64)
7844                .sum::<u64>()
7845        );
7846        assert_eq!(
7847            projected_stats.chain_fallback_filter_equivalents,
7848            projected_chain_fallbacks
7849                .iter()
7850                .map(|route| route.4 as u64)
7851                .sum::<u64>()
7852        );
7853        let expected_semantic_scans =
7854            projected_scans as u64 + projected_stats.chain_fallback_scan_equivalents;
7855        let expected_semantic_filters =
7856            projected_filters as u64 + projected_stats.chain_fallback_filter_equivalents;
7857        eprintln!(
7858            "dependency-closed ordinary reference: physical_scans={projected_scans} physical_filters={projected_filters} chain_fallback_scan_equivalents={} chain_fallback_filter_equivalents={} semantic_scans={expected_semantic_scans} semantic_filters={expected_semantic_filters}",
7859            projected_stats.chain_fallback_scan_equivalents,
7860            projected_stats.chain_fallback_filter_equivalents,
7861        );
7862        drop(projected_baseline);
7863        assert!(
7864            baseline_scans >= 9_000,
7865            "unexpected baseline scan count: {baseline_scans}"
7866        );
7867        assert!(
7868            baseline_filters >= 7_000,
7869            "unexpected baseline filter count: {baseline_filters}"
7870        );
7871        drop(baseline);
7872        assert_eq!(
7873            program.resident_certification_initializations(),
7874            1,
7875            "external certificate audits and the disabled-resident baseline must reuse the compile-time certification"
7876        );
7877
7878        const WARMUP_RUNS: usize = 2;
7879        const MEASURED_RUNS: usize = 5;
7880        let latency_diagnostics_enabled = resident_latency_diagnostics_enabled()?;
7881        let mut warmup_seconds = Vec::with_capacity(WARMUP_RUNS);
7882        let mut warmup_device_seconds = Vec::with_capacity(WARMUP_RUNS);
7883        let mut resident_seconds = Vec::with_capacity(MEASURED_RUNS);
7884        let mut device_seconds = Vec::with_capacity(MEASURED_RUNS);
7885        for run in 0..(WARMUP_RUNS + MEASURED_RUNS) {
7886            let started = std::time::Instant::now();
7887            let resident = {
7888                let _env = if latency_diagnostics_enabled {
7889                    ResidentEnvGuard::set(&[
7890                        ("XLOG_REQUIRE_RESIDENT_RECURSION", "1"),
7891                        (RESIDENT_LATENCY_DIAGNOSTICS_ENV, "1"),
7892                    ])
7893                } else {
7894                    ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")])
7895                };
7896                program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
7897            };
7898            assert_eq!(
7899                program.resident_certification_initializations(),
7900                1,
7901                "resident corpus run {run} must reuse the single cached certification"
7902            );
7903            let elapsed_seconds = started.elapsed().as_secs_f64();
7904            if run < WARMUP_RUNS {
7905                warmup_seconds.push(elapsed_seconds);
7906            } else {
7907                resident_seconds.push(elapsed_seconds);
7908            }
7909            assert_eq!(
7910                snapshot_query_results(provider.as_ref(), &resident)?,
7911                baseline_snapshot,
7912                "resident corpus run {run} changed query semantics"
7913            );
7914
7915            let resident_stats = resident.stats.as_ref().expect("resident profile");
7916            let resident_physical_scans = op_count(resident_stats, "scan");
7917            let resident_physical_filters = op_count(resident_stats, "filter");
7918            let graph = resident_stats
7919                .resident_graph
7920                .as_ref()
7921                .expect("resident selection telemetry");
7922            eprintln!(
7923                "resident operation profile run {run}: semantic_scans={} semantic_filters={} physical_scans={resident_physical_scans} physical_filters={resident_physical_filters}",
7924                graph.semantic_scan_invocations, graph.semantic_filter_invocations
7925            );
7926            assert_eq!(graph.semantic_scan_invocations, expected_semantic_scans);
7927            assert_eq!(graph.semantic_filter_invocations, expected_semantic_filters);
7928            assert_eq!(
7929                resident_physical_scans as u64,
7930                graph.device_scan_invocations
7931            );
7932            assert_eq!(
7933                resident_physical_filters as u64,
7934                graph.device_filter_invocations
7935            );
7936            assert_eq!(
7937                graph.selection,
7938                ResidentGraphSelectionKind::ResidentConditionalGraph
7939            );
7940            assert_eq!(graph.conditional_graph_launches, 1);
7941            assert_eq!(graph.terminal_synchronizations, 1);
7942            assert_eq!(graph.host_iterations, 0);
7943            assert_eq!(graph.host_allocations, 0);
7944            assert_eq!(graph.host_status_injections, 0);
7945            assert_eq!(graph.deterministic_d2h_violations, 0);
7946            assert_eq!(graph.host_dispatched_scan_ops, 0);
7947            assert_eq!(graph.host_dispatched_filter_ops, 0);
7948            assert!(graph.device_scan_invocations >= graph.semantic_scan_invocations);
7949            assert!(graph.device_filter_invocations >= graph.semantic_filter_invocations);
7950            assert_eq!(
7951                graph.deferred_profile.timed_scan_filter_invocations,
7952                graph.device_scan_invocations + graph.device_filter_invocations
7953            );
7954            assert!(graph.deferred_profile.device_elapsed_ns > 0);
7955            assert_eq!(graph.deferred_profile.final_sync_misattributed_ns, 0);
7956            let device_elapsed_seconds =
7957                graph.deferred_profile.device_elapsed_ns as f64 / 1_000_000_000.0;
7958            if run < WARMUP_RUNS {
7959                warmup_device_seconds.push(device_elapsed_seconds);
7960            } else {
7961                device_seconds.push(device_elapsed_seconds);
7962            }
7963            assert_eq!(graph.core_transfers.tracked_htod_calls, 0);
7964            assert_eq!(graph.core_transfers.tracked_htod_bytes, 0);
7965            assert_eq!(graph.core_transfers.tracked_dtoh_calls, 0);
7966            assert_eq!(graph.core_transfers.tracked_dtoh_bytes, 0);
7967            assert_eq!(graph.core_transfers.provider_dtoh_calls, 0);
7968            assert_eq!(graph.core_transfers.untracked_metadata_dtoh_calls, 0);
7969            assert_eq!(graph.final_observation.dtoh_calls, 1);
7970            assert_eq!(
7971                graph.final_observation.dtoh_bytes,
7972                60 + 8 * graph.staged_store_mutations
7973            );
7974            assert_eq!(graph.final_observation.pinned_receipts, 1);
7975            let json = resident_stats.format_json();
7976            assert!(json.contains("\"resident_graph\""), "{json}");
7977            assert!(
7978                json.contains("\"selection\":\"resident_conditional_graph\""),
7979                "{json}"
7980            );
7981            assert!(
7982                json.contains(&format!(
7983                    "\"semantic_scan_invocations\":{expected_semantic_scans}"
7984                )),
7985                "{json}"
7986            );
7987            assert!(
7988                json.contains(&format!(
7989                    "\"semantic_filter_invocations\":{expected_semantic_filters}"
7990                )),
7991                "{json}"
7992            );
7993            drop(resident);
7994        }
7995        assert_eq!(program.resident_certification_initializations(), 1);
7996        assert_eq!(warmup_seconds.len(), WARMUP_RUNS);
7997        assert_eq!(resident_seconds.len(), MEASURED_RUNS);
7998        let compile_plus_first_resident_seconds =
7999            compile_and_certification_seconds + warmup_seconds[0];
8000        let max_seconds = warmup_seconds
8001            .iter()
8002            .chain(&resident_seconds)
8003            .copied()
8004            .fold(0.0_f64, f64::max);
8005        let mut sorted_resident_seconds = resident_seconds.clone();
8006        let median_seconds = median_seconds(&mut sorted_resident_seconds);
8007        eprintln!(
8008            "resident corpus latency: compile_and_certification_seconds={compile_and_certification_seconds:.6} compile_plus_first_resident_seconds={compile_plus_first_resident_seconds:.6} warmup_end_to_end_seconds={warmup_seconds:?} measured_end_to_end_seconds={resident_seconds:?} warmup_device_event_seconds={warmup_device_seconds:?} measured_device_event_seconds={device_seconds:?} median_measured_end_to_end_seconds={median_seconds:.6} max_all_end_to_end_seconds={max_seconds:.6}"
8009        );
8010        assert!(
8011            median_seconds <= 1.25,
8012            "five-run steady-state resident corpus median {median_seconds:.6}s exceeds 1.25s: {resident_seconds:?}"
8013        );
8014        assert!(
8015            max_seconds <= 1.75,
8016            "seven-run resident corpus max {max_seconds:.6}s exceeds 1.75s: warmup={warmup_seconds:?} measured={resident_seconds:?}"
8017        );
8018        Ok(())
8019    }
8020
8021    #[test]
8022    #[ignore = "requires the exact external issue corpus checkout and serialized CUDA"]
8023    fn pinned_corpus_resident_latency_phase_diagnostic() -> Result<()> {
8024        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8025        let corpus = std::path::PathBuf::from(
8026            std::env::var("XLOG_PINNED_CORPUS_ROOT")
8027                .expect("XLOG_PINNED_CORPUS_ROOT must name the pinned corpus checkout"),
8028        );
8029        assert_exact_clean_corpus(&corpus);
8030        let compile_started = std::time::Instant::now();
8031        let program = corpus_program(&corpus)?;
8032        eprintln!(
8033            "resident latency setup: compile_ns={}",
8034            u64::try_from(compile_started.elapsed().as_nanos()).unwrap_or(u64::MAX)
8035        );
8036        let Some(provider) = pinned_corpus_test_provider() else {
8037            return Ok(());
8038        };
8039        let baseline = {
8040            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
8041            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
8042        };
8043        let expected = snapshot_query_results(provider.as_ref(), &baseline)?;
8044        drop(baseline);
8045
8046        RESIDENT_LATENCY_SAMPLE.store(0, Ordering::Relaxed);
8047        for run in 0..5 {
8048            let evaluate_started = std::time::Instant::now();
8049            let resident = {
8050                let _env = ResidentEnvGuard::set(&[
8051                    ("XLOG_REQUIRE_RESIDENT_RECURSION", "1"),
8052                    (RESIDENT_LATENCY_DIAGNOSTICS_ENV, "1"),
8053                ]);
8054                program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
8055            };
8056            let evaluate_return_ns =
8057                u64::try_from(evaluate_started.elapsed().as_nanos()).unwrap_or(u64::MAX);
8058            assert_eq!(
8059                snapshot_query_results(provider.as_ref(), &resident)?,
8060                expected,
8061                "resident latency diagnostic run {run} changed query semantics"
8062            );
8063            let graph = resident
8064                .stats
8065                .as_ref()
8066                .and_then(|stats| stats.resident_graph.as_ref())
8067                .expect("resident latency diagnostic telemetry");
8068            assert_eq!(
8069                graph.selection,
8070                ResidentGraphSelectionKind::ResidentConditionalGraph
8071            );
8072            let query_buffers = resident.queries.len();
8073            let manager_bytes_before_result_drop = provider.memory().allocated_bytes();
8074            let result_drop_started = std::time::Instant::now();
8075            drop(resident);
8076            let result_drop_ns =
8077                u64::try_from(result_drop_started.elapsed().as_nanos()).unwrap_or(u64::MAX);
8078            let manager_bytes_after_result_drop = provider.memory().allocated_bytes();
8079            let result_manager_bytes_released =
8080                manager_bytes_before_result_drop.saturating_sub(manager_bytes_after_result_drop);
8081            eprintln!(
8082                "resident latency result teardown: sample={run} evaluate_return_ns={evaluate_return_ns} result_drop_ns={result_drop_ns} query_buffers={query_buffers} manager_bytes_before={manager_bytes_before_result_drop} manager_bytes_after={manager_bytes_after_result_drop} manager_bytes_released={result_manager_bytes_released} deallocation_calls=unavailable"
8083            );
8084        }
8085        Ok(())
8086    }
8087
8088    #[test]
8089    #[ignore = "requires a serialized release-mode CUDA acceptance run"]
8090    fn resident_disconnected_four_thousand_rule_scaling_acceptance() -> Result<()> {
8091        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8092        let corpus = std::path::PathBuf::from(
8093            std::env::var("XLOG_PINNED_CORPUS_ROOT")
8094                .expect("XLOG_PINNED_CORPUS_ROOT must name the pinned corpus checkout"),
8095        );
8096        assert_exact_clean_corpus(&corpus);
8097        let base_program = corpus_program(&corpus)?;
8098        let entry = corpus.join("scenarios/acceptance/issue1/q01_blind.xlog");
8099        let mut augmented_source = std::fs::read_to_string(&entry).map_err(|error| {
8100            XlogError::Execution(format!("failed to read {}: {error}", entry.display()))
8101        })?;
8102        augmented_source.push_str("\npred disconnected_seed(u32).\n");
8103        for family in 0..4_000 {
8104            augmented_source.push_str(&format!("pred disconnected_family_{family}(u32).\n"));
8105            augmented_source.push_str(&format!(
8106                "disconnected_family_{family}(X) :- disconnected_seed(X).\n"
8107            ));
8108        }
8109        let resolver = xlog_logic::compile::load_modules(&entry, vec![corpus.join("programs")])
8110            .map_err(|error| XlogError::Compilation(error.to_string()))?;
8111        let augmented_program = LogicProgram::compile_with_resolver(&augmented_source, &resolver)?;
8112        let Some(provider) = pinned_corpus_test_provider() else {
8113            return Ok(());
8114        };
8115
8116        const WARMUP_PAIRS: usize = 2;
8117        const MEASURED_PAIRS: usize = 5;
8118
8119        let mut warmup_base_seconds = Vec::with_capacity(WARMUP_PAIRS);
8120        let mut warmup_augmented_seconds = Vec::with_capacity(WARMUP_PAIRS);
8121        let mut base_seconds = Vec::with_capacity(MEASURED_PAIRS);
8122        let mut augmented_seconds = Vec::with_capacity(MEASURED_PAIRS);
8123        let mut expected_snapshot = None;
8124        let mut expected_profile = None;
8125        for pair in 0..(WARMUP_PAIRS + MEASURED_PAIRS) {
8126            let augmented_order = if pair % 2 == 0 {
8127                [false, true]
8128            } else {
8129                [true, false]
8130            };
8131            for augmented in augmented_order {
8132                let program = if augmented {
8133                    &augmented_program
8134                } else {
8135                    &base_program
8136                };
8137                let started = std::time::Instant::now();
8138                let result = {
8139                    let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
8140                    program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
8141                };
8142                let elapsed = started.elapsed().as_secs_f64();
8143                match (pair < WARMUP_PAIRS, augmented) {
8144                    (true, false) => warmup_base_seconds.push(elapsed),
8145                    (true, true) => warmup_augmented_seconds.push(elapsed),
8146                    (false, false) => base_seconds.push(elapsed),
8147                    (false, true) => augmented_seconds.push(elapsed),
8148                }
8149
8150                let sample_kind = if augmented { "augmented" } else { "base" };
8151                let snapshot = snapshot_query_results(provider.as_ref(), &result)?;
8152                if let Some(expected) = &expected_snapshot {
8153                    assert_eq!(
8154                        &snapshot, expected,
8155                        "{sample_kind} resident pair {pair} changed query output"
8156                    );
8157                } else {
8158                    assert!(
8159                        !augmented,
8160                        "first resident scaling sample must be the base plan"
8161                    );
8162                    expected_snapshot = Some(snapshot);
8163                }
8164
8165                let stats = result.stats.as_ref().expect("resident scaling profile");
8166                let graph = stats
8167                    .resident_graph
8168                    .as_ref()
8169                    .expect("resident scaling graph telemetry");
8170                assert_eq!(
8171                    graph.selection,
8172                    ResidentGraphSelectionKind::ResidentConditionalGraph
8173                );
8174                assert_eq!(graph.conditional_graph_launches, 1);
8175                assert_eq!(
8176                    op_count(stats, "scan") as u64,
8177                    graph.device_scan_invocations
8178                );
8179                assert_eq!(
8180                    op_count(stats, "filter") as u64,
8181                    graph.device_filter_invocations
8182                );
8183                let profile = (
8184                    strata_op_profile(stats),
8185                    graph.device_scan_invocations,
8186                    graph.device_filter_invocations,
8187                    graph.semantic_scan_invocations,
8188                    graph.semantic_filter_invocations,
8189                    graph.deferred_profile.timed_scan_filter_invocations,
8190                );
8191                if let Some(expected) = &expected_profile {
8192                    assert_eq!(
8193                        &profile, expected,
8194                        "{sample_kind} resident pair {pair} changed semantic or device op counts"
8195                    );
8196                } else {
8197                    assert!(
8198                        !augmented,
8199                        "first resident scaling profile must be the base plan"
8200                    );
8201                    expected_profile = Some(profile);
8202                }
8203                drop(result);
8204            }
8205        }
8206
8207        let mut base_median_samples = base_seconds.clone();
8208        let mut augmented_median_samples = augmented_seconds.clone();
8209        let base_median = median_seconds(&mut base_median_samples);
8210        let augmented_median = median_seconds(&mut augmented_median_samples);
8211        let paired_deltas = augmented_seconds
8212            .iter()
8213            .zip(&base_seconds)
8214            .map(|(augmented, base)| augmented - base)
8215            .collect::<Vec<_>>();
8216        let mut paired_delta_samples = paired_deltas.clone();
8217        let paired_delta_median = median_seconds(&mut paired_delta_samples);
8218        let allowed_delta = (base_median * 0.10).max(0.100);
8219        eprintln!(
8220            "disconnected 4,000-rule resident timings: warmup_base={warmup_base_seconds:?} warmup_augmented={warmup_augmented_seconds:?} measured_base={base_seconds:?} measured_augmented={augmented_seconds:?} paired_deltas={paired_deltas:?} base_median={base_median:.6}s augmented_median={augmented_median:.6}s paired_delta_median={paired_delta_median:.6}s allowed_delta={allowed_delta:.6}s"
8221        );
8222        assert!(
8223            augmented_median - base_median <= allowed_delta,
8224            "disconnected 4,000-rule resident median delta {:.6}s exceeds {:.6}s: base={base_seconds:?} augmented={augmented_seconds:?}",
8225            augmented_median - base_median,
8226            allowed_delta,
8227        );
8228        assert!(
8229            paired_delta_median <= allowed_delta,
8230            "disconnected 4,000-rule resident paired median delta {paired_delta_median:.6}s exceeds {allowed_delta:.6}s: base={base_seconds:?} augmented={augmented_seconds:?} paired={paired_deltas:?}"
8231        );
8232        Ok(())
8233    }
8234
8235    fn assert_required_resident_semantics(
8236        source: &str,
8237        case: &str,
8238        expected_query_schema: Option<&Schema>,
8239        expected_query_types: Option<&[ScalarType]>,
8240        expected_query_rows: Option<usize>,
8241    ) -> Result<()> {
8242        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8243        let program = LogicProgram::compile(source)?;
8244        let Some(provider) = ground_term_encoding_test_provider() else {
8245            return Ok(());
8246        };
8247        let baseline = {
8248            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
8249            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
8250        };
8251        let expected = snapshot_query_results(provider.as_ref(), &baseline)?;
8252        if let Some(expected_query_schema) = expected_query_schema {
8253            assert_eq!(expected.len(), 1, "schema witness must have one query");
8254            assert_eq!(
8255                &expected[0].schema, expected_query_schema,
8256                "pre-change legacy query schema witness changed for {case}"
8257            );
8258        }
8259        if let Some(expected_query_types) = expected_query_types {
8260            assert_eq!(expected.len(), 1, "type witness must have one query");
8261            assert_eq!(
8262                expected[0]
8263                    .schema
8264                    .columns
8265                    .iter()
8266                    .map(|(_, scalar)| *scalar)
8267                    .collect::<Vec<_>>(),
8268                expected_query_types,
8269                "pre-change legacy query types changed for {case}"
8270            );
8271        }
8272        if let Some(expected_query_rows) = expected_query_rows {
8273            assert_eq!(expected.len(), 1, "row witness must have one query");
8274            assert_eq!(
8275                expected[0].rows.len(),
8276                expected_query_rows,
8277                "pre-change legacy query row count changed for {case}"
8278            );
8279        }
8280        drop(baseline);
8281        let resident = {
8282            let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
8283            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
8284        };
8285        assert_eq!(
8286            snapshot_query_results(provider.as_ref(), &resident)?,
8287            expected,
8288            "resident semantic case {case} diverged"
8289        );
8290        let graph = resident
8291            .stats
8292            .as_ref()
8293            .and_then(|stats| stats.resident_graph.as_ref())
8294            .expect("resident semantic telemetry");
8295        assert_eq!(
8296            graph.selection,
8297            ResidentGraphSelectionKind::ResidentConditionalGraph,
8298            "resident semantic case {case} did not use the production graph"
8299        );
8300        assert_eq!(graph.conditional_graph_launches, 1);
8301        assert_eq!(graph.core_transfers.tracked_htod_calls, 0);
8302        assert_eq!(graph.core_transfers.tracked_htod_bytes, 0);
8303        assert_eq!(graph.core_transfers.tracked_dtoh_calls, 0);
8304        assert_eq!(graph.core_transfers.tracked_dtoh_bytes, 0);
8305        assert_eq!(graph.core_transfers.provider_dtoh_calls, 0);
8306        assert_eq!(graph.core_transfers.untracked_metadata_dtoh_calls, 0);
8307        assert_eq!(graph.deterministic_d2h_violations, 0);
8308        assert_eq!(graph.final_observation.dtoh_calls, 1);
8309        assert_eq!(graph.final_observation.pinned_receipts, 1);
8310        Ok(())
8311    }
8312
8313    fn program_with_authored_query_prefix(
8314        source: &str,
8315        original_head: &str,
8316        authored_head: &str,
8317    ) -> Result<Program> {
8318        let mut program = xlog_logic::parse_program(source)?;
8319        let mut renamed_declarations = 0usize;
8320        for declaration in &mut program.predicates {
8321            if declaration.name == original_head {
8322                declaration.name = authored_head.to_string();
8323                renamed_declarations += 1;
8324            }
8325        }
8326        let mut renamed_rules = 0usize;
8327        for rule in &mut program.rules {
8328            if rule.head.predicate == original_head {
8329                rule.head.predicate = authored_head.to_string();
8330                renamed_rules += 1;
8331            }
8332        }
8333        let mut renamed_queries = 0usize;
8334        for query in &mut program.queries {
8335            if query.atom.predicate == original_head {
8336                query.atom.predicate = authored_head.to_string();
8337                renamed_queries += 1;
8338            }
8339        }
8340        assert_eq!(renamed_declarations, 1);
8341        assert!(renamed_rules >= 1);
8342        assert_eq!(renamed_queries, 1);
8343        Ok(program)
8344    }
8345
8346    fn compile_program_with_authored_query_prefix(
8347        source: &str,
8348        original_head: &str,
8349        authored_head: &str,
8350    ) -> Result<LogicProgram> {
8351        LogicProgram::compile_program(program_with_authored_query_prefix(
8352            source,
8353            original_head,
8354            authored_head,
8355        )?)
8356    }
8357
8358    fn assert_required_resident_authored_prefix_semantics(source: &str, case: &str) -> Result<()> {
8359        const AUTHORED_HEAD: &str = "__xlog_query_authored";
8360        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8361        let program = compile_program_with_authored_query_prefix(source, "answer", AUTHORED_HEAD)?;
8362        let Some(provider) = ground_term_encoding_test_provider() else {
8363            return Ok(());
8364        };
8365        let explicit_schema = Schema::new(vec![("external_value".to_string(), ScalarType::Symbol)]);
8366        let baseline = {
8367            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
8368            program.evaluate_with_options(
8369                provider.clone(),
8370                HashMap::from([(
8371                    AUTHORED_HEAD.to_string(),
8372                    provider.create_empty_buffer(explicit_schema.clone())?,
8373                )]),
8374                true,
8375            )?
8376        };
8377        let expected = snapshot_query_results(provider.as_ref(), &baseline)?;
8378        assert_eq!(expected.len(), 1);
8379        drop(baseline);
8380        let resident = {
8381            let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
8382            program.evaluate_with_options(
8383                provider.clone(),
8384                HashMap::from([(
8385                    AUTHORED_HEAD.to_string(),
8386                    provider.create_empty_buffer(explicit_schema)?,
8387                )]),
8388                true,
8389            )?
8390        };
8391        assert_eq!(
8392            snapshot_query_results(provider.as_ref(), &resident)?,
8393            expected,
8394            "resident authored-prefix case {case} diverged"
8395        );
8396        let graph = resident
8397            .stats
8398            .as_ref()
8399            .and_then(|stats| stats.resident_graph.as_ref())
8400            .expect("resident authored-prefix telemetry");
8401        assert_eq!(
8402            graph.selection,
8403            ResidentGraphSelectionKind::ResidentConditionalGraph,
8404            "resident authored-prefix case {case} declined"
8405        );
8406        assert_eq!(graph.conditional_graph_launches, 1);
8407        Ok(())
8408    }
8409
8410    #[test]
8411    fn required_resident_preserves_programmatic_authored_query_prefix_empty_input() -> Result<()> {
8412        assert_required_resident_authored_prefix_semantics(
8413            r#"
8414                pred source(symbol).
8415                pred answer(symbol).
8416                answer(X) :- source(X).
8417                ?- answer(X).
8418            "#,
8419            "one authored rule",
8420        )
8421    }
8422
8423    #[test]
8424    fn required_resident_does_not_treat_programmatic_authored_prefix_rules_as_generated(
8425    ) -> Result<()> {
8426        assert_required_resident_authored_prefix_semantics(
8427            r#"
8428                pred left_source(symbol).
8429                pred right_source(symbol).
8430                pred answer(symbol).
8431                answer(X) :- left_source(X).
8432                answer(X) :- right_source(X).
8433                ?- answer(X).
8434            "#,
8435            "two authored rules",
8436        )
8437    }
8438
8439    #[test]
8440    fn compile_program_rejects_exact_generated_query_head_collision() -> Result<()> {
8441        let program = program_with_authored_query_prefix(
8442            r#"
8443                pred source(symbol).
8444                pred answer(symbol).
8445                answer(X) :- source(X).
8446                ?- answer(X).
8447            "#,
8448            "answer",
8449            "__xlog_query_0",
8450        )?;
8451        let error = match LogicProgram::compile_program(program) {
8452            Ok(_) => panic!("exact compiler-generated query head collision must be rejected"),
8453            Err(error) => error,
8454        };
8455        assert!(
8456            error
8457                .to_string()
8458                .contains("authored relation __xlog_query_0 collides with generated query head"),
8459            "unexpected collision error: {error}"
8460        );
8461        Ok(())
8462    }
8463
8464    #[test]
8465    fn compiler_generated_query_relation_validation_is_exact_and_host_only() -> Result<()> {
8466        let program = LogicProgram::compile(
8467            r#"
8468                pred source(symbol).
8469                ?- source(X).
8470            "#,
8471        )?;
8472
8473        let error = program
8474            .reject_compiler_generated_query_relation_names(["__xlog_query_0"], "persistent caller")
8475            .expect_err("an exact compiler-generated query head must be rejected");
8476        match error {
8477            XlogError::Execution(message) => assert_eq!(
8478                message,
8479                "persistent caller relation __xlog_query_0 collides with generated query head"
8480            ),
8481            other => panic!("expected typed execution rejection, got {other:?}"),
8482        }
8483
8484        program.reject_compiler_generated_query_relation_names(
8485            ["__xlog_query_authored"],
8486            "persistent caller",
8487        )?;
8488        Ok(())
8489    }
8490
8491    #[test]
8492    fn compiler_generated_query_relation_validation_rejects_provenance_mutation() -> Result<()> {
8493        let source = r#"
8494            pred source(symbol).
8495            ?- source(X).
8496        "#;
8497
8498        let mut omitted = LogicProgram::compile(source)?;
8499        let LogicExecutionPlan::Ordinary(plan) = &mut omitted.plan else {
8500            panic!("ordinary program must compile to an ordinary plan");
8501        };
8502        plan.generated_query_rules.clear();
8503        let error = omitted
8504            .reject_compiler_generated_query_relation_names(std::iter::empty(), "caller input")
8505            .expect_err("omitted compiler provenance must be rejected");
8506        assert!(error.to_string().contains(
8507            "compiler-generated query provenance count 0 does not match authored query count 1"
8508        ));
8509
8510        let mut repositioned = LogicProgram::compile(source)?;
8511        let LogicExecutionPlan::Ordinary(plan) = &mut repositioned.plan else {
8512            panic!("ordinary program must compile to an ordinary plan");
8513        };
8514        plan.generated_query_rules[0].query_index = 1;
8515        let error = repositioned
8516            .reject_compiler_generated_query_relation_names(std::iter::empty(), "caller input")
8517            .expect_err("repositioned compiler provenance must be rejected");
8518        assert!(error
8519            .to_string()
8520            .contains("compiler-generated query provenance position 0 carries query index 1"));
8521
8522        let mut renamed = LogicProgram::compile(source)?;
8523        let LogicExecutionPlan::Ordinary(plan) = &mut renamed.plan else {
8524            panic!("ordinary program must compile to an ordinary plan");
8525        };
8526        let provenance = &plan.generated_query_rules[0];
8527        plan.rules_by_scc[provenance.scc_index][provenance.rule_index].head =
8528            "__xlog_query_spoof".to_string();
8529        let error = renamed
8530            .reject_compiler_generated_query_relation_names(std::iter::empty(), "caller input")
8531            .expect_err("renamed compiler-generated query head must be rejected");
8532        assert!(error.to_string().contains(
8533            "compiler-generated query provenance 0 expects head __xlog_query_0 but references authored head __xlog_query_spoof"
8534        ));
8535        Ok(())
8536    }
8537
8538    #[test]
8539    fn cloned_program_shares_reusable_state_identity_but_recompile_does_not() -> Result<()> {
8540        let source = r#"
8541            pred source(symbol).
8542            ?- source(X).
8543        "#;
8544        let original = LogicProgram::compile(source)?;
8545        let cloned = original.clone();
8546        let recompiled = LogicProgram::compile(source)?;
8547
8548        cloned.validate_reusable_state_identity(
8549            &original.reusable_state_identity,
8550            "materialized cache",
8551        )?;
8552        let error = recompiled
8553            .validate_reusable_state_identity(
8554                &original.reusable_state_identity,
8555                "materialized cache",
8556            )
8557            .expect_err("independent compilation must have a distinct reusable-state identity");
8558        assert!(matches!(error, XlogError::Execution(_)));
8559        assert_eq!(
8560            error.to_string(),
8561            "Execution error: materialized cache belongs to a different compiled logic program"
8562        );
8563        Ok(())
8564    }
8565
8566    #[test]
8567    fn foreign_cache_and_runtime_are_rejected_before_evaluation_work() -> Result<()> {
8568        let source = r#"
8569            pred source(u32).
8570            pred out(u32).
8571            out(X) :- source(X).
8572            ?- out(X).
8573        "#;
8574        let program = LogicProgram::compile(source)?;
8575        let foreign_program = LogicProgram::compile(source)?;
8576        let Some(provider) = ground_term_encoding_test_provider() else {
8577            return Ok(());
8578        };
8579        let base_store = program.create_relation_store(provider.clone())?;
8580        let (_, cache) =
8581            program.evaluate_with_relation_store_and_cache(provider.clone(), &base_store, false)?;
8582        let allocations_before_cache = provider.memory().alloc_count();
8583
8584        let error = match foreign_program.evaluate_cached_relation_store(provider.clone(), &cache) {
8585            Ok(_) => panic!("independently compiled program must reject a foreign cache"),
8586            Err(error) => error,
8587        };
8588        assert_eq!(
8589            error.to_string(),
8590            "Execution error: materialized cache belongs to a different compiled logic program"
8591        );
8592        assert_eq!(provider.memory().alloc_count(), allocations_before_cache);
8593
8594        let mut runtime = program.create_session_runtime(provider.clone(), &base_store, false)?;
8595        let runtime_store_before = std::ptr::from_ref(runtime.executor.store());
8596        let source_version_before = runtime.executor.store().version("source");
8597        let allocations_before_runtime = provider.memory().alloc_count();
8598        let error =
8599            match foreign_program.evaluate_with_session_runtime(provider.clone(), &mut runtime) {
8600                Ok(_) => panic!("independently compiled program must reject a foreign runtime"),
8601                Err(error) => error,
8602            };
8603        assert_eq!(
8604            error.to_string(),
8605            "Execution error: session runtime belongs to a different compiled logic program"
8606        );
8607        assert_eq!(provider.memory().alloc_count(), allocations_before_runtime);
8608        assert_eq!(
8609            std::ptr::from_ref(runtime.executor.store()),
8610            runtime_store_before
8611        );
8612        assert_eq!(
8613            runtime.executor.store().version("source"),
8614            source_version_before
8615        );
8616        Ok(())
8617    }
8618
8619    #[test]
8620    fn foreign_reusable_state_is_rejected_before_delta_take_or_device_work() -> Result<()> {
8621        let source = r#"
8622            pred source(u32).
8623            pred out(u32).
8624            source(1).
8625            out(X) :- source(X).
8626            ?- out(X).
8627        "#;
8628        let program = LogicProgram::compile(source)?;
8629        let foreign_program = LogicProgram::compile(source)?;
8630        let Some(provider) = ground_term_encoding_test_provider() else {
8631            return Ok(());
8632        };
8633        let mut base_store = program.create_relation_store(provider.clone())?;
8634        let (_, initial_cache) =
8635            program.evaluate_with_relation_store_and_cache(provider.clone(), &base_store, false)?;
8636        let mut cache = Some(initial_cache);
8637        let mut runtime = None;
8638        let cache_before = cache.as_ref().map(std::ptr::from_ref);
8639        let source_version_before = base_store.version("source");
8640        let mut raw_delta_store = program.create_relation_store(provider.clone())?;
8641        let raw_insert = raw_delta_store
8642            .remove("source")
8643            .expect("inline source fact must materialize a nonempty raw delta");
8644        let raw_deltas = HashMap::from([(
8645            "source".to_string(),
8646            RelationDelta::new(Some(raw_insert), None),
8647        )]);
8648        let allocations_before_raw = provider.memory().alloc_count();
8649
8650        let error = match foreign_program.prepare_relation_deltas_commit_with_session_runtime(
8651            provider.clone(),
8652            &mut base_store,
8653            &mut cache,
8654            &mut runtime,
8655            raw_deltas,
8656        ) {
8657            Ok(_) => panic!("raw delta preparation must reject a foreign cache"),
8658            Err(error) => error,
8659        };
8660        assert_eq!(
8661            error.to_string(),
8662            "Execution error: materialized cache belongs to a different compiled logic program"
8663        );
8664        assert_eq!(provider.memory().alloc_count(), allocations_before_raw);
8665        assert_eq!(cache.as_ref().map(std::ptr::from_ref), cache_before);
8666        assert!(runtime.is_none());
8667        assert_eq!(base_store.version("source"), source_version_before);
8668
8669        let mut no_cache = None;
8670        let mut foreign_runtime =
8671            Some(program.create_session_runtime(provider.clone(), &base_store, false)?);
8672        let runtime_before = foreign_runtime.as_ref().map(std::ptr::from_ref);
8673        let mut prepared_delta_store = program.create_relation_store(provider.clone())?;
8674        let prepared_insert = prepared_delta_store
8675            .remove("source")
8676            .expect("inline source fact must materialize a nonempty prepared delta");
8677        let prepared_batch = program.prepare_relation_delta_batch(
8678            provider.as_ref(),
8679            vec![(
8680                "source".to_string(),
8681                RelationDelta::new(Some(prepared_insert), None),
8682            )],
8683            &BTreeSet::new(),
8684        )?;
8685        let allocations_before_prepared = provider.memory().alloc_count();
8686        let error = match foreign_program.prepare_relation_delta_commit_with_session_runtime(
8687            provider.clone(),
8688            &mut base_store,
8689            &mut no_cache,
8690            &mut foreign_runtime,
8691            prepared_batch,
8692        ) {
8693            Ok(_) => panic!("prepared delta commit must reject a foreign runtime"),
8694            Err(error) => error,
8695        };
8696        assert_eq!(
8697            error.to_string(),
8698            "Execution error: session runtime belongs to a different compiled logic program"
8699        );
8700        assert_eq!(provider.memory().alloc_count(), allocations_before_prepared);
8701        assert!(no_cache.is_none());
8702        assert_eq!(
8703            foreign_runtime.as_ref().map(std::ptr::from_ref),
8704            runtime_before
8705        );
8706        assert_eq!(base_store.version("source"), source_version_before);
8707
8708        let mut ordered_delta_store = program.create_relation_store(provider.clone())?;
8709        let ordered_insert = ordered_delta_store
8710            .remove("source")
8711            .expect("inline source fact must materialize a nonempty ordered delta");
8712        let allocations_before_ordered = provider.memory().alloc_count();
8713        let error = match foreign_program.apply_relation_delta_batch(
8714            provider.clone(),
8715            &mut base_store,
8716            &mut cache,
8717            vec![(
8718                "source".to_string(),
8719                RelationDelta::new(Some(ordered_insert), None),
8720            )],
8721        ) {
8722            Ok(_) => panic!("ordered delta application must reject a foreign cache"),
8723            Err(error) => error,
8724        };
8725        assert_eq!(
8726            error.to_string(),
8727            "Execution error: materialized cache belongs to a different compiled logic program"
8728        );
8729        assert_eq!(
8730            provider.memory().alloc_count(),
8731            allocations_before_ordered,
8732            "identity validation must run before ordered device coalescing"
8733        );
8734        assert_eq!(cache.as_ref().map(std::ptr::from_ref), cache_before);
8735        assert_eq!(base_store.version("source"), source_version_before);
8736        Ok(())
8737    }
8738
8739    #[test]
8740    fn persistent_relation_store_rejects_generated_query_head_before_setup() -> Result<()> {
8741        let program = LogicProgram::compile(
8742            r#"
8743                pred source(symbol).
8744                ?- source(X).
8745            "#,
8746        )?;
8747        let Some(provider) = ground_term_encoding_test_provider() else {
8748            return Ok(());
8749        };
8750        let mut store = program.create_relation_store(provider.clone())?;
8751        let query_schema = program
8752            .schemas
8753            .get("__xlog_query_0")
8754            .expect("compiler-generated query schema")
8755            .clone();
8756        store.put(
8757            "__xlog_query_0",
8758            provider.create_empty_buffer(query_schema)?,
8759        );
8760        let mut store_before = store
8761            .names()
8762            .map(|name| {
8763                (
8764                    name.to_string(),
8765                    store.get(name).expect("named relation").num_rows(),
8766                )
8767            })
8768            .collect::<Vec<_>>();
8769        store_before.sort_unstable();
8770        let allocations_before = provider.memory().alloc_count();
8771
8772        let error = match program.evaluate_with_relation_store(provider.clone(), &store, false) {
8773            Ok(_) => panic!("persistent caller must not seed a generated query head"),
8774            Err(error) => error,
8775        };
8776        assert!(matches!(error, XlogError::Execution(_)));
8777        assert!(error.to_string().contains(
8778            "persistent caller relation __xlog_query_0 collides with generated query head"
8779        ));
8780        assert_eq!(provider.memory().alloc_count(), allocations_before);
8781        let mut store_after = store
8782            .names()
8783            .map(|name| {
8784                (
8785                    name.to_string(),
8786                    store.get(name).expect("named relation").num_rows(),
8787                )
8788            })
8789            .collect::<Vec<_>>();
8790        store_after.sort_unstable();
8791        assert_eq!(store_after, store_before);
8792        Ok(())
8793    }
8794
8795    #[test]
8796    fn persistent_session_rejects_generated_query_head_before_setup() -> Result<()> {
8797        let program = LogicProgram::compile(
8798            r#"
8799                pred source(symbol).
8800                ?- source(X).
8801            "#,
8802        )?;
8803        let Some(provider) = ground_term_encoding_test_provider() else {
8804            return Ok(());
8805        };
8806        let mut store = program.create_relation_store(provider.clone())?;
8807        let query_schema = program
8808            .schemas
8809            .get("__xlog_query_0")
8810            .expect("compiler-generated query schema")
8811            .clone();
8812        store.put(
8813            "__xlog_query_0",
8814            provider.create_empty_buffer(query_schema)?,
8815        );
8816        let mut store_before = store
8817            .names()
8818            .map(|name| {
8819                (
8820                    name.to_string(),
8821                    store.get(name).expect("named relation").num_rows(),
8822                )
8823            })
8824            .collect::<Vec<_>>();
8825        store_before.sort_unstable();
8826        let allocations_before = provider.memory().alloc_count();
8827
8828        let error = match program.create_session_runtime(provider.clone(), &store, false) {
8829            Ok(_) => panic!("persistent session must not seed a generated query head"),
8830            Err(error) => error,
8831        };
8832        assert!(matches!(error, XlogError::Execution(_)));
8833        assert!(error.to_string().contains(
8834            "persistent caller relation __xlog_query_0 collides with generated query head"
8835        ));
8836        assert_eq!(provider.memory().alloc_count(), allocations_before);
8837        let mut store_after = store
8838            .names()
8839            .map(|name| {
8840                (
8841                    name.to_string(),
8842                    store.get(name).expect("named relation").num_rows(),
8843                )
8844            })
8845            .collect::<Vec<_>>();
8846        store_after.sort_unstable();
8847        assert_eq!(store_after, store_before);
8848        Ok(())
8849    }
8850
8851    #[test]
8852    fn raw_delta_preparation_rejects_generated_query_head_without_consuming_state() -> Result<()> {
8853        let program = LogicProgram::compile(
8854            r#"
8855                pred source(symbol).
8856                ?- source(X).
8857            "#,
8858        )?;
8859        let Some(provider) = ground_term_encoding_test_provider() else {
8860            return Ok(());
8861        };
8862        let mut store = program.create_relation_store(provider.clone())?;
8863        let (_, cached_store) =
8864            program.evaluate_with_relation_store_and_cache(provider.clone(), &store, false)?;
8865        let mut cached_store = Some(cached_store);
8866        let cached_store_before = cached_store.as_ref().map(std::ptr::from_ref);
8867        let mut session_runtime = None;
8868        let mut store_before = store
8869            .names()
8870            .map(|name| {
8871                (
8872                    name.to_string(),
8873                    store.get(name).expect("named relation").num_rows(),
8874                )
8875            })
8876            .collect::<Vec<_>>();
8877        store_before.sort_unstable();
8878        let allocations_before = provider.memory().alloc_count();
8879        let deltas =
8880            HashMap::from([("__xlog_query_0".to_string(), RelationDelta::new(None, None))]);
8881
8882        let error = match program.prepare_relation_deltas_commit_with_session_runtime(
8883            provider.clone(),
8884            &mut store,
8885            &mut cached_store,
8886            &mut session_runtime,
8887            deltas,
8888        ) {
8889            Ok(_) => panic!("delta preparation must reject a generated query head"),
8890            Err(error) => error,
8891        };
8892        assert!(matches!(error, XlogError::Execution(_)));
8893        assert!(error
8894            .to_string()
8895            .contains("caller delta relation __xlog_query_0 collides with generated query head"));
8896        assert_eq!(provider.memory().alloc_count(), allocations_before);
8897        assert_eq!(
8898            cached_store.as_ref().map(std::ptr::from_ref),
8899            cached_store_before
8900        );
8901        assert!(session_runtime.is_none());
8902        let mut store_after = store
8903            .names()
8904            .map(|name| {
8905                (
8906                    name.to_string(),
8907                    store.get(name).expect("named relation").num_rows(),
8908                )
8909            })
8910            .collect::<Vec<_>>();
8911        store_after.sort_unstable();
8912        assert_eq!(store_after, store_before);
8913        Ok(())
8914    }
8915
8916    #[test]
8917    fn ordered_delta_preparation_rejects_generated_query_head_before_device_work() -> Result<()> {
8918        let program = LogicProgram::compile(
8919            r#"
8920                pred source(symbol).
8921                source("payload").
8922                ?- source(X).
8923            "#,
8924        )?;
8925        let Some(provider) = ground_term_encoding_test_provider() else {
8926            return Ok(());
8927        };
8928        let mut insert_store = program.create_relation_store(provider.clone())?;
8929        let insert = insert_store
8930            .remove("source")
8931            .expect("inline source fact must materialize a nonempty insert");
8932        let mut delete_store = program.create_relation_store(provider.clone())?;
8933        let delete = delete_store
8934            .remove("source")
8935            .expect("inline source fact must materialize a nonempty delete");
8936        let allocations_before = provider.memory().alloc_count();
8937
8938        let error = match program.prepare_relation_delta_batch(
8939            provider.as_ref(),
8940            vec![
8941                (
8942                    "__xlog_query_0".to_string(),
8943                    RelationDelta::new(Some(insert), None),
8944                ),
8945                (
8946                    "__xlog_query_0".to_string(),
8947                    RelationDelta::new(None, Some(delete)),
8948                ),
8949            ],
8950            &BTreeSet::from(["__xlog_query_0".to_string()]),
8951        ) {
8952            Ok(_) => panic!("ordered delta preparation must reject a generated query head"),
8953            Err(error) => error,
8954        };
8955        assert!(matches!(error, XlogError::Execution(_)));
8956        assert!(error
8957            .to_string()
8958            .contains("caller delta relation __xlog_query_0 collides with generated query head"));
8959        assert_eq!(provider.memory().alloc_count(), allocations_before);
8960        Ok(())
8961    }
8962
8963    #[test]
8964    fn resident_rejects_caller_input_for_exact_generated_query_head_before_setup() -> Result<()> {
8965        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8966        let program = LogicProgram::compile(
8967            r#"
8968                pred source(symbol).
8969                ?- source(X).
8970            "#,
8971        )?;
8972        let Some(provider) = ground_term_encoding_test_provider() else {
8973            return Ok(());
8974        };
8975        let generated_schema = program
8976            .schemas
8977            .get("__xlog_query_0")
8978            .expect("generated query schema")
8979            .clone();
8980        for policy in [
8981            &[][..],
8982            &[("XLOG_DISABLE_RESIDENT_RECURSION", "1")][..],
8983            &[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")][..],
8984        ] {
8985            let input = provider.create_empty_buffer(generated_schema.clone())?;
8986            let allocations_before = provider.memory().alloc_count();
8987            let _env = ResidentEnvGuard::set(policy);
8988            let error = match program.evaluate_with_options(
8989                provider.clone(),
8990                HashMap::from([("__xlog_query_0".to_string(), input)]),
8991                true,
8992            ) {
8993                Ok(_) => panic!("caller input must not occupy a generated query head"),
8994                Err(error) => error,
8995            };
8996            assert!(
8997                error.to_string().contains(
8998                    "caller input relation __xlog_query_0 collides with generated query head"
8999                ),
9000                "unexpected caller-input collision error: {error}"
9001            );
9002            assert_eq!(
9003                provider.memory().alloc_count(),
9004                allocations_before,
9005                "generated-head collision must fail before resident setup"
9006            );
9007        }
9008        Ok(())
9009    }
9010
9011    #[test]
9012    fn resident_executor_distinguishes_derived_placeholders_from_explicit_empty_inputs(
9013    ) -> Result<()> {
9014        let program = LogicProgram::compile(
9015            r#"
9016                pred source(symbol).
9017                pred answer(symbol, symbol).
9018                pred seeded_derived(symbol).
9019                seeded_derived("seed").
9020                seeded_derived(X) :- source(X).
9021                answer("yes", X) :- source(X).
9022                ?- answer(Outcome, Claim).
9023            "#,
9024        )?;
9025        let LogicExecutionPlan::Ordinary(plan) = &program.plan else {
9026            panic!("ordinary test program must compile to an ordinary plan");
9027        };
9028        let Some(provider) = ground_term_encoding_test_provider() else {
9029            return Ok(());
9030        };
9031
9032        let compiler_seeded =
9033            program.prepare_resident_executor(&provider, HashMap::new(), false, plan)?;
9034        assert!(compiler_seeded.store().get("source").is_some());
9035        assert!(compiler_seeded.store().get("answer").is_none());
9036        assert_eq!(
9037            compiler_seeded
9038                .store()
9039                .get("seeded_derived")
9040                .expect("derived relation with inline facts must be seeded")
9041                .cached_row_count(),
9042            Some(1),
9043        );
9044
9045        let explicit_schema = Schema::new(vec![
9046            ("external_outcome".into(), ScalarType::Symbol),
9047            ("external_claim".into(), ScalarType::Symbol),
9048        ]);
9049        let explicit_empty = provider.create_empty_buffer(explicit_schema.clone())?;
9050        let explicitly_seeded = program.prepare_resident_executor(
9051            &provider,
9052            HashMap::from([("answer".to_string(), explicit_empty)]),
9053            false,
9054            plan,
9055        )?;
9056        assert_eq!(
9057            explicitly_seeded
9058                .store()
9059                .get("answer")
9060                .expect("explicit empty input must be retained")
9061                .schema(),
9062            &explicit_schema,
9063        );
9064        Ok(())
9065    }
9066
9067    #[test]
9068    fn required_resident_query_schema_matches_legacy_for_nonempty_result() -> Result<()> {
9069        let expected_schema = Schema::new(vec![
9070            ("computed_0".into(), ScalarType::Symbol),
9071            ("c0".into(), ScalarType::Symbol),
9072        ]);
9073        assert_required_resident_semantics(
9074            r#"
9075                pred source(symbol).
9076                pred answer(symbol, symbol).
9077                source("claim").
9078                answer("yes", X) :- source(X).
9079                ?- answer(Outcome, Claim).
9080            "#,
9081            "nonempty synthetic query schema",
9082            Some(&expected_schema),
9083            None,
9084            None,
9085        )
9086    }
9087
9088    #[test]
9089    fn required_resident_query_schema_matches_legacy_for_empty_result() -> Result<()> {
9090        let expected_schema = Schema::new(vec![
9091            ("computed_0".into(), ScalarType::Symbol),
9092            ("c0".into(), ScalarType::Symbol),
9093        ]);
9094        assert_required_resident_semantics(
9095            r#"
9096                pred source(symbol).
9097                pred answer(symbol, symbol).
9098                answer("yes", X) :- source(X).
9099                ?- answer(Outcome, Claim).
9100            "#,
9101            "empty synthetic query schema",
9102            Some(&expected_schema),
9103            None,
9104            None,
9105        )
9106    }
9107
9108    #[test]
9109    fn required_resident_recursive_query_schema_matches_legacy() -> Result<()> {
9110        let expected_schema = Schema::new(vec![("c0".into(), ScalarType::U32)]);
9111        assert_required_resident_semantics(
9112            r#"
9113                pred seed(u32).
9114                pred edge(u32, u32).
9115                pred reach(u32).
9116                seed(1).
9117                edge(1, 2).
9118                reach(X) :- seed(X).
9119                reach(X) :- reach(X), edge(X, Y).
9120                ?- reach(X).
9121            "#,
9122            "recursive synthetic query schema",
9123            Some(&expected_schema),
9124            None,
9125            None,
9126        )
9127    }
9128
9129    #[test]
9130    fn required_resident_recursive_constant_projection_schema_matches_legacy() -> Result<()> {
9131        let expected_schema = Schema::new(vec![
9132            ("item".into(), ScalarType::U32),
9133            ("computed_1".into(), ScalarType::U32),
9134        ]);
9135        assert_required_resident_semantics(
9136            r#"
9137                pred seed(item: u32).
9138                pred path(item: u32, category: u32).
9139                seed(7).
9140                path(X, 1) :- seed(X).
9141                path(X, 2) :- path(X, 1).
9142                ?- path(Item, Category).
9143            "#,
9144            "recursive constant-projection schema",
9145            Some(&expected_schema),
9146            Some(&[ScalarType::U32, ScalarType::U32]),
9147            Some(2),
9148        )
9149    }
9150
9151    #[test]
9152    #[ignore = "requires a serialized release-mode CUDA acceptance run"]
9153    fn resident_semantic_acceptance_matrix() -> Result<()> {
9154        let ordinary_cases = [
9155            (
9156                "recursion",
9157                r#"
9158                    pred edge(u32, u32).
9159                    pred reach(u32, u32).
9160                    edge(1, 2). edge(2, 3).
9161                    reach(X, Y) :- edge(X, Y).
9162                    reach(X, Z) :- reach(X, Y), edge(Y, Z).
9163                    ?- reach(X, Y).
9164                "#,
9165            ),
9166            (
9167                "negation",
9168                r#"
9169                    pred item(u32). pred blocked(u32). pred visible(u32).
9170                    item(1). item(2). blocked(2).
9171                    visible(X) :- item(X), not blocked(X).
9172                    ?- visible(X).
9173                "#,
9174            ),
9175            (
9176                "constraint",
9177                r#"
9178                    pred safe(u32).
9179                    safe(1).
9180                    :- safe(2).
9181                    ?- safe(X).
9182                "#,
9183            ),
9184            (
9185                "multiple queries",
9186                r#"
9187                    pred seed(u32). pred left(u32). pred right(u32).
9188                    seed(7).
9189                    left(X) :- seed(X).
9190                    right(X) :- seed(X).
9191                    ?- left(X).
9192                    ?- right(X).
9193                "#,
9194            ),
9195            (
9196                "nullary set",
9197                r#"
9198                    pred ready(). pred answer().
9199                    ready().
9200                    answer() :- ready().
9201                    ?- answer().
9202                "#,
9203            ),
9204            (
9205                "same name with multiple arities",
9206                r#"
9207                    pred item(u32). pred item(u32, u32).
9208                    pred unary(u32). pred binary(u32, u32).
9209                    item(1). item(1, 2).
9210                    unary(X) :- item(X).
9211                    binary(X, Y) :- item(X, Y).
9212                    ?- unary(X).
9213                    ?- binary(X, Y).
9214                "#,
9215            ),
9216        ];
9217        for (case, source) in ordinary_cases {
9218            assert_required_resident_semantics(source, case, None, None, None)?;
9219        }
9220
9221        let output_cases: [(&str, &str, &[ScalarType], usize); 7] = [
9222            (
9223                "zero-row output",
9224                r#"
9225                    pred empty(u32).
9226                    ?- empty(X).
9227                "#,
9228                &[ScalarType::U32],
9229                0,
9230            ),
9231            (
9232                "identity output",
9233                r#"
9234                    pred source(u32). pred copied(u32).
9235                    source(2). source(1).
9236                    copied(X) :- source(X).
9237                    ?- copied(X).
9238                "#,
9239                &[ScalarType::U32],
9240                2,
9241            ),
9242            (
9243                "u64 output",
9244                r#"
9245                    pred wide(u64).
9246                    wide(5000000000).
9247                    ?- wide(X).
9248                "#,
9249                &[ScalarType::U64],
9250                1,
9251            ),
9252            (
9253                "symbol output",
9254                r#"
9255                    pred labeled(symbol).
9256                    labeled("claim").
9257                    ?- labeled(X).
9258                "#,
9259                &[ScalarType::Symbol],
9260                1,
9261            ),
9262            (
9263                "mixed output",
9264                r#"
9265                    pred mixed(u32, u64, symbol).
9266                    mixed(7, 5000000000, "claim").
9267                    ?- mixed(Small, Wide, Label).
9268                "#,
9269                &[ScalarType::U32, ScalarType::U64, ScalarType::Symbol],
9270                1,
9271            ),
9272            (
9273                "nullary output",
9274                r#"
9275                    pred ready(). pred answer().
9276                    ready().
9277                    answer() :- ready().
9278                    ?- answer().
9279                "#,
9280                &[],
9281                1,
9282            ),
9283            (
9284                "arity-seventeen output",
9285                r#"
9286                    pred wide(u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32).
9287                    wide(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17).
9288                    ?- wide(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q).
9289                "#,
9290                &[ScalarType::U32; 17],
9291                1,
9292            ),
9293        ];
9294        for (case, source, expected_types, expected_rows) in output_cases {
9295            assert_required_resident_semantics(
9296                source,
9297                case,
9298                None,
9299                Some(expected_types),
9300                Some(expected_rows),
9301            )?;
9302        }
9303
9304        assert!(
9305            LogicProgram::compile(
9306                r#"
9307                    pred seed(u32). pred invalid(u32).
9308                    seed(1).
9309                    invalid(X) :- seed(X), X = "wrong type".
9310                    ?- seed(X).
9311                "#,
9312            )
9313            .is_err(),
9314            "invalid unreachable rules must still be validated"
9315        );
9316
9317        required_resident_evaluation_canonicalizes_a_caller_input_only_relation()?;
9318        complete_store_evaluation_declines_resident_partial_execution_and_materializes_all_heads()?;
9319
9320        {
9321            let _env_lock = resident_env_lock().lock().expect("resident env lock");
9322            let Some(provider) = ground_term_encoding_test_provider() else {
9323                return Ok(());
9324            };
9325            let program = LogicProgram::compile(
9326                r#"
9327                    pred seed(u32). pred out(u32).
9328                    seed(1). out(X) :- seed(X). ?- out(X).
9329                "#,
9330            )?;
9331            let store = program.create_relation_store(provider.clone())?;
9332            let mut session = program.create_session_runtime(provider.clone(), &store, true)?;
9333            let (result, _) = program.evaluate_with_session_runtime(provider, &mut session)?;
9334            let graph = result
9335                .stats
9336                .as_ref()
9337                .and_then(|stats| stats.resident_graph.as_ref())
9338                .expect("session resident decline telemetry");
9339            assert_eq!(graph.selection, ResidentGraphSelectionKind::ExistingGpu);
9340            assert_eq!(
9341                graph.decline,
9342                Some(ResidentGraphDeclineReason::FullStoreRequested)
9343            );
9344            assert_eq!(graph.conditional_graph_launches, 0);
9345        }
9346
9347        {
9348            let _env_lock = resident_env_lock().lock().expect("resident env lock");
9349            let Some(provider) = ground_term_encoding_test_provider() else {
9350                return Ok(());
9351            };
9352            let program = LogicProgram::compile(
9353                r#"
9354                    pred p(u32). pred q(u32).
9355                    p(1). q(X) :- p(X), know p(X). ?- q(X).
9356                "#,
9357            )?;
9358            let baseline = {
9359                let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
9360                program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
9361            };
9362            let expected = snapshot_query_results(provider.as_ref(), &baseline)?;
9363            drop(baseline);
9364            let automatic =
9365                program.evaluate_with_options(provider.clone(), HashMap::new(), true)?;
9366            assert_eq!(
9367                snapshot_query_results(provider.as_ref(), &automatic)?,
9368                expected
9369            );
9370            let graph = automatic
9371                .stats
9372                .as_ref()
9373                .and_then(|stats| stats.resident_graph.as_ref())
9374                .expect("nonordinary resident decline telemetry");
9375            assert_eq!(graph.selection, ResidentGraphSelectionKind::ExistingGpu);
9376            assert_eq!(
9377                graph.decline,
9378                Some(ResidentGraphDeclineReason::NonOrdinaryPlan)
9379            );
9380            assert_eq!(graph.conditional_graph_launches, 0);
9381        }
9382        Ok(())
9383    }
9384
9385    #[test]
9386    fn complete_store_evaluation_declines_resident_partial_execution_and_materializes_all_heads(
9387    ) -> Result<()> {
9388        let _env_lock = resident_env_lock().lock().expect("resident env lock");
9389        let Some(provider) = ground_term_encoding_test_provider() else {
9390            return Ok(());
9391        };
9392        let program = LogicProgram::compile(
9393            r#"
9394                pred base(u32).
9395                pred edge(u32, u32).
9396                pred queried(u32).
9397                pred disconnected(u32).
9398                base(7).
9399                edge(7, 8).
9400                queried(X) :- base(X).
9401                queried(Y) :- queried(X), edge(X, Y).
9402                disconnected(X) :- base(X).
9403                ?- queried(X).
9404            "#,
9405        )?;
9406        let seed = program.create_relation_store(provider.clone())?;
9407        let (result, store) =
9408            program.evaluate_with_relation_store_and_cache(provider.clone(), &seed, true)?;
9409
9410        assert_eq!(
9411            provider.download_column::<u32>(&result.queries[0].buffer, 0)?,
9412            vec![7, 8]
9413        );
9414        assert_eq!(
9415            provider.download_column::<u32>(
9416                store
9417                    .as_relation_store()
9418                    .get("disconnected")
9419                    .expect("complete disconnected head"),
9420                0,
9421            )?,
9422            vec![7]
9423        );
9424        let graph = result
9425            .stats
9426            .as_ref()
9427            .and_then(|stats| stats.resident_graph.as_ref())
9428            .expect("resident decline telemetry");
9429        assert_eq!(graph.selection, ResidentGraphSelectionKind::ExistingGpu);
9430        assert_eq!(
9431            graph.decline,
9432            Some(ResidentGraphDeclineReason::FullStoreRequested)
9433        );
9434        assert_eq!(graph.conditional_graph_launches, 0);
9435        assert_eq!(graph.staged_store_mutations, 0);
9436        assert!(result
9437            .stats
9438            .as_ref()
9439            .expect("profile")
9440            .format_json()
9441            .contains("resident_graph_declined"));
9442        Ok(())
9443    }
9444
9445    #[test]
9446    fn no_query_program_bypasses_resident_certification_and_executes_the_full_plan() -> Result<()> {
9447        let _env_lock = resident_env_lock().lock().expect("resident env lock");
9448        let program = LogicProgram::compile(
9449            r#"
9450                pred seed(u32).
9451                pred disconnected(u32).
9452                seed(1).
9453                disconnected(X) :- seed(X).
9454            "#,
9455        )?;
9456        assert_eq!(program.resident_certification_initializations(), 0);
9457        let Some(provider) = ground_term_encoding_test_provider() else {
9458            return Ok(());
9459        };
9460
9461        let baseline = {
9462            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
9463            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
9464        };
9465        assert!(baseline.queries.is_empty());
9466        let baseline_profile =
9467            strata_op_profile(baseline.stats.as_ref().expect("baseline profile"));
9468        assert_eq!(op_count(baseline.stats.as_ref().unwrap(), "scan"), 1);
9469        assert_eq!(program.resident_certification_initializations(), 0);
9470        drop(baseline);
9471
9472        let automatic = program.evaluate_with_options(provider.clone(), HashMap::new(), true)?;
9473        assert!(automatic.queries.is_empty());
9474        assert_eq!(
9475            strata_op_profile(automatic.stats.as_ref().expect("automatic profile")),
9476            baseline_profile
9477        );
9478        let graph = automatic
9479            .stats
9480            .as_ref()
9481            .and_then(|stats| stats.resident_graph.as_ref())
9482            .expect("resident decline telemetry");
9483        assert_eq!(graph.selection, ResidentGraphSelectionKind::ExistingGpu);
9484        assert_eq!(
9485            graph.decline,
9486            Some(ResidentGraphDeclineReason::FullStoreRequested)
9487        );
9488        assert_eq!(graph.conditional_graph_launches, 0);
9489        assert_eq!(program.resident_certification_initializations(), 0);
9490        drop(automatic);
9491
9492        let required_result = {
9493            let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
9494            program.evaluate_with_options(provider, HashMap::new(), true)
9495        };
9496        let required = match required_result {
9497            Ok(_) => panic!("no-query evaluation cannot use the resident partial-result route"),
9498            Err(error) => error,
9499        };
9500        assert!(required
9501            .to_string()
9502            .contains("resident conditional-graph execution was required but declined"));
9503        assert!(required.to_string().contains("FullStoreRequested"));
9504        assert_eq!(program.resident_certification_initializations(), 0);
9505        Ok(())
9506    }
9507
9508    #[test]
9509    fn automatic_resident_decline_executes_the_untouched_full_plan() -> Result<()> {
9510        let _env_lock = resident_env_lock().lock().expect("resident env lock");
9511        let Some(provider) = ground_term_encoding_test_provider() else {
9512            return Ok(());
9513        };
9514        let program = LogicProgram::compile(
9515            r#"
9516                pred unsupported(f64).
9517                pred seed(u32).
9518                pred disconnected(u32).
9519                unsupported(7.5).
9520                seed(1).
9521                disconnected(X) :- seed(X).
9522                ?- unsupported(X).
9523            "#,
9524        )?;
9525        let baseline = {
9526            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
9527            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
9528        };
9529        let baseline_snapshot = snapshot_query_results(provider.as_ref(), &baseline)?;
9530        let baseline_profile =
9531            strata_op_profile(baseline.stats.as_ref().expect("baseline profile"));
9532        assert_eq!(op_count(baseline.stats.as_ref().unwrap(), "scan"), 2);
9533        drop(baseline);
9534
9535        let automatic = program.evaluate_with_options(provider.clone(), HashMap::new(), true)?;
9536        assert_eq!(
9537            snapshot_query_results(provider.as_ref(), &automatic)?,
9538            baseline_snapshot
9539        );
9540        assert_eq!(
9541            strata_op_profile(automatic.stats.as_ref().expect("automatic profile")),
9542            baseline_profile
9543        );
9544        let graph = automatic
9545            .stats
9546            .as_ref()
9547            .and_then(|stats| stats.resident_graph.as_ref())
9548            .expect("resident decline telemetry");
9549        assert_eq!(graph.selection, ResidentGraphSelectionKind::ExistingGpu);
9550        assert!(graph.decline.is_some());
9551        assert_eq!(graph.conditional_graph_launches, 0);
9552        Ok(())
9553    }
9554
9555    #[test]
9556    fn ordinary_unsupported_scalar_types_decline_before_launch() -> Result<()> {
9557        let _env_lock = resident_env_lock().lock().expect("resident env lock");
9558        let Some(provider) = ground_term_encoding_test_provider() else {
9559            return Ok(());
9560        };
9561        let programs = [
9562            r#"
9563                pred unsupported(i64).
9564                unsupported(7).
9565                ?- unsupported(X).
9566            "#,
9567            r#"
9568                pred unsupported(f64).
9569                unsupported(7.5).
9570                ?- unsupported(X).
9571            "#,
9572            r#"
9573                pred unsupported(u32, f64).
9574                unsupported(7, 8.5).
9575                ?- unsupported(X, Y).
9576            "#,
9577        ];
9578        for source in programs {
9579            let program = LogicProgram::compile(source)?;
9580            let automatic =
9581                program.evaluate_with_options(provider.clone(), HashMap::new(), true)?;
9582            assert_eq!(automatic.queries[0].buffer.cached_row_count(), Some(1));
9583            let graph = automatic
9584                .stats
9585                .as_ref()
9586                .and_then(|stats| stats.resident_graph.as_ref())
9587                .expect("resident decline telemetry");
9588            assert_eq!(graph.selection, ResidentGraphSelectionKind::ExistingGpu);
9589            assert!(graph.decline.is_some());
9590            assert_eq!(graph.conditional_graph_launches, 0);
9591            drop(automatic);
9592
9593            let error = {
9594                let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
9595                match program.evaluate_with_options(provider.clone(), HashMap::new(), true) {
9596                    Ok(_) => {
9597                        panic!("required resident execution must reject a prelaunch decline")
9598                    }
9599                    Err(error) => error,
9600                }
9601            };
9602            assert!(error
9603                .to_string()
9604                .contains("resident conditional-graph execution was required but declined"));
9605            assert_eq!(program.resident_certification_initializations(), 1);
9606        }
9607
9608        let mut cached_failure = LogicProgram::compile(
9609            r#"
9610                pred output(u32).
9611                output(7).
9612                ?- output(X).
9613            "#,
9614        )?;
9615        assert_eq!(cached_failure.resident_certification_initializations(), 1);
9616        cached_failure.reusable_state_identity = Arc::new(LogicProgramIdentity::new());
9617        cached_failure
9618            .reusable_state_identity
9619            .get_or_init_resident_certification(|| -> Result<ResidentGraphCertifiedPlan> {
9620                Err(XlogError::Execution(
9621                    "deterministic certification failure".into(),
9622                ))
9623            })
9624            .expect_err("injected certification must fail");
9625        let automatic =
9626            cached_failure.evaluate_with_options(provider.clone(), HashMap::new(), true)?;
9627        assert_eq!(automatic.queries[0].buffer.cached_row_count(), Some(1));
9628        let decline = automatic
9629            .stats
9630            .as_ref()
9631            .and_then(|stats| stats.resident_graph.as_ref())
9632            .and_then(|stats| stats.decline.as_ref())
9633            .expect("automatic certification failure must report its decline");
9634        assert!(format!("{decline:?}").contains("deterministic certification failure"));
9635        drop(automatic);
9636
9637        let required = {
9638            let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
9639            match cached_failure.evaluate_with_options(provider, HashMap::new(), true) {
9640                Ok(_) => panic!("required resident execution must reject cached certification"),
9641                Err(error) => error,
9642            }
9643        };
9644        assert!(required
9645            .to_string()
9646            .contains("resident conditional-graph execution was required but declined"));
9647        assert!(required
9648            .to_string()
9649            .contains("deterministic certification failure"));
9650        assert_eq!(cached_failure.resident_certification_initializations(), 1);
9651        Ok(())
9652    }
9653
9654    #[test]
9655    fn ordinary_evaluation_uses_the_required_resident_graph_on_the_callers_cuda_context(
9656    ) -> Result<()> {
9657        let _env_lock = resident_env_lock().lock().expect("resident env lock");
9658        let Some(provider) = ground_term_encoding_test_provider() else {
9659            return Ok(());
9660        };
9661        assert!(provider.memory().runtime().is_some());
9662        let program = LogicProgram::compile(
9663            r#"
9664                pred seed(u32).
9665                pred edge(u32, u32).
9666                pred reach(u32).
9667                seed(1).
9668                edge(1, 2).
9669                edge(2, 3).
9670                reach(X) :- seed(X).
9671                reach(Y) :- reach(X), edge(X, Y), Y >= 2.
9672                ?- reach(X).
9673            "#,
9674        )?;
9675        let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
9676        let result = program.evaluate_with_options(provider.clone(), HashMap::new(), true)?;
9677        assert_eq!(
9678            provider.download_column::<u32>(&result.queries[0].buffer, 0)?,
9679            vec![1, 2, 3]
9680        );
9681        let graph = result
9682            .stats
9683            .as_ref()
9684            .and_then(|stats| stats.resident_graph.as_ref())
9685            .expect("resident selection telemetry");
9686        assert_eq!(
9687            graph.selection,
9688            ResidentGraphSelectionKind::ResidentConditionalGraph
9689        );
9690        assert_eq!(graph.conditional_graph_launches, 1);
9691        assert!(graph.device_scan_invocations > 0);
9692        assert!(graph.device_filter_invocations > 0);
9693        assert_eq!(graph.core_transfers.tracked_htod_calls, 0);
9694        assert_eq!(graph.core_transfers.tracked_dtoh_calls, 0);
9695        assert_eq!(graph.core_transfers.provider_dtoh_calls, 0);
9696        assert_eq!(graph.core_transfers.untracked_metadata_dtoh_calls, 0);
9697        assert_eq!(graph.final_observation.dtoh_calls, 1);
9698        assert_eq!(graph.final_observation.pinned_receipts, 1);
9699        assert!(graph.deferred_profile.device_elapsed_ns > 0);
9700        Ok(())
9701    }
9702
9703    #[test]
9704    fn required_resident_evaluation_canonicalizes_a_caller_input_only_relation() -> Result<()> {
9705        let _env_lock = resident_env_lock().lock().expect("resident env lock");
9706        let Some(provider) = ground_term_encoding_test_provider() else {
9707            return Ok(());
9708        };
9709        let program = LogicProgram::compile(
9710            r#"
9711                pred input(u32).
9712                pred output(u32).
9713                output(X) :- input(X).
9714                ?- output(X).
9715            "#,
9716        )?;
9717        let input = provider.create_buffer_from_slice::<u32>(
9718            &[2, 1, 2],
9719            Schema::new(vec![("x".into(), ScalarType::U32)]),
9720        )?;
9721        assert!(!input.canonical_full_row_set_certified());
9722        let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
9723        let result = program.evaluate_with_options(
9724            provider.clone(),
9725            HashMap::from([("input".to_string(), input)]),
9726            true,
9727        )?;
9728        assert_eq!(
9729            provider.download_column::<u32>(&result.queries[0].buffer, 0)?,
9730            vec![1, 2]
9731        );
9732        let graph = result
9733            .stats
9734            .as_ref()
9735            .and_then(|stats| stats.resident_graph.as_ref())
9736            .expect("resident selection telemetry");
9737        assert_eq!(
9738            graph.selection,
9739            ResidentGraphSelectionKind::ResidentConditionalGraph
9740        );
9741        assert_eq!(graph.conditional_graph_launches, 1);
9742        Ok(())
9743    }
9744
9745    #[test]
9746    fn incompatible_caller_input_type_fails_before_resident_allocation_or_launch() -> Result<()> {
9747        let _env_lock = resident_env_lock().lock().expect("resident env lock");
9748        let Some(provider) = ground_term_encoding_test_provider() else {
9749            return Ok(());
9750        };
9751        let program = LogicProgram::compile(
9752            r#"
9753                pred input(u32).
9754                pred output(u32).
9755                output(X) :- input(X).
9756                ?- output(X).
9757            "#,
9758        )?;
9759        let input = provider.create_buffer_from_slice::<u64>(
9760            &[1],
9761            Schema::new(vec![("x".into(), ScalarType::U64)]),
9762        )?;
9763        provider.memory().reset_alloc_count();
9764        let allocated_before = provider.memory().allocated_bytes();
9765        let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
9766        let error = match program.evaluate_with_options(
9767            provider.clone(),
9768            HashMap::from([("input".to_string(), input)]),
9769            false,
9770        ) {
9771            Err(error) => error,
9772            Ok(_) => panic!("incompatible input type unexpectedly reached resident setup"),
9773        };
9774        assert!(error.to_string().contains("schema mismatch"));
9775        assert!(provider.memory().allocated_bytes() <= allocated_before);
9776        assert_eq!(provider.memory().alloc_count(), 0);
9777        assert!(provider.memory().runtime().is_some());
9778        Ok(())
9779    }
9780
9781    #[test]
9782    fn malformed_caller_device_count_fails_before_resident_allocation_or_launch() -> Result<()> {
9783        let _env_lock = resident_env_lock().lock().expect("resident env lock");
9784        let Some(provider) = ground_term_encoding_test_provider() else {
9785            return Ok(());
9786        };
9787        let program = LogicProgram::compile(
9788            r#"
9789                pred input(u32).
9790                pred output(u32).
9791                output(X) :- input(X).
9792                ?- output(X).
9793            "#,
9794        )?;
9795        for (selection, policy) in [
9796            ("automatic", &[][..]),
9797            ("required", &[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")][..]),
9798        ] {
9799            let mut column = provider.memory().alloc::<u8>(4)?;
9800            provider
9801                .device()
9802                .inner()
9803                .htod_sync_copy_into(&7u32.to_ne_bytes(), &mut column)
9804                .map_err(|error| XlogError::Kernel(error.to_string()))?;
9805            let mut device_count = provider.memory().alloc::<u32>(1)?;
9806            provider
9807                .device()
9808                .inner()
9809                .htod_sync_copy_into(&[2], &mut device_count)
9810                .map_err(|error| XlogError::Kernel(error.to_string()))?;
9811            let input = CudaBuffer::from_columns(
9812                vec![column.into()],
9813                1,
9814                device_count,
9815                Schema::new(vec![("x".into(), ScalarType::U32)]),
9816            );
9817            provider.memory().reset_alloc_count();
9818            let allocated_before = provider.memory().allocated_bytes();
9819            let _env = ResidentEnvGuard::set(policy);
9820            let error = match program.evaluate_with_options(
9821                provider.clone(),
9822                HashMap::from([("input".to_string(), input)]),
9823                false,
9824            ) {
9825                Err(error) => error,
9826                Ok(_) => panic!("malformed input count unexpectedly reached resident setup"),
9827            };
9828            assert!(
9829                error
9830                    .to_string()
9831                    .contains("Logical row count 2 exceeds row capacity 1"),
9832                "unexpected error for {selection}: {error}"
9833            );
9834            assert!(provider.memory().allocated_bytes() <= allocated_before);
9835            assert_eq!(provider.memory().alloc_count(), 0);
9836            assert!(provider.memory().runtime().is_some());
9837        }
9838        Ok(())
9839    }
9840
9841    #[test]
9842    fn program_fact_loader_uses_shared_ground_term_encoding() -> Result<()> {
9843        let Some(provider) = ground_term_encoding_test_provider() else {
9844            return Ok(());
9845        };
9846        let program = LogicProgram::compile(
9847            r#"
9848                pred encoded(u32, u64, i32, i64, f32, f64, bool, bool, symbol, symbol).
9849                encoded(42, 43, -44, -45, 1.5, 2.25, true, 0, "hello", world).
9850            "#,
9851        )?;
9852        let store = program.create_relation_store(provider.clone())?;
9853        let encoded = store
9854            .get("encoded")
9855            .ok_or_else(|| XlogError::Execution("missing encoded fact buffer".to_string()))?;
9856
9857        assert_eq!(provider.download_column::<u32>(encoded, 0)?, vec![42]);
9858        assert_eq!(provider.download_column::<u64>(encoded, 1)?, vec![43]);
9859        assert_eq!(provider.download_column::<i32>(encoded, 2)?, vec![-44]);
9860        assert_eq!(provider.download_column::<i64>(encoded, 3)?, vec![-45]);
9861        assert_eq!(provider.download_column::<f32>(encoded, 4)?, vec![1.5]);
9862        assert_eq!(provider.download_column::<f64>(encoded, 5)?, vec![2.25]);
9863        assert_eq!(provider.download_column::<bool>(encoded, 6)?, vec![true]);
9864        assert_eq!(provider.download_column::<bool>(encoded, 7)?, vec![false]);
9865        assert_eq!(
9866            provider.download_column::<u32>(encoded, 8)?,
9867            vec![symbol::intern("hello")]
9868        );
9869        assert_eq!(
9870            provider.download_column::<u32>(encoded, 9)?,
9871            vec![symbol::intern("world")]
9872        );
9873
9874        let invalid = LogicProgram::compile(
9875            r#"
9876                pred invalid(u32).
9877                invalid(X).
9878            "#,
9879        )?;
9880        let error = match invalid.create_relation_store(provider) {
9881            Ok(_) => panic!("a variable in a fact must be rejected"),
9882            Err(error) => error,
9883        };
9884        let XlogError::Execution(message) = error else {
9885            panic!("fact encoding must remain an Execution error, got {error:?}");
9886        };
9887        assert_eq!(
9888            message,
9889            "Failed to encode fact for predicate invalid at column 0: Fact cannot contain variable X"
9890        );
9891        Ok(())
9892    }
9893
9894    #[test]
9895    fn grouped_facts_preserve_fact_and_rule_results_without_seed_operations() -> Result<()> {
9896        let Some(provider) = ground_term_encoding_test_provider() else {
9897            return Ok(());
9898        };
9899        let program = LogicProgram::compile(
9900            r#"
9901                pred base(u32).
9902                pred derived(u32).
9903                base(1).
9904                base(1).
9905                base(2).
9906                derived(X) :- base(X).
9907                ?- base(X).
9908                ?- derived(X).
9909            "#,
9910        )?;
9911        let LogicExecutionPlan::Ordinary(plan) = &program.plan else {
9912            panic!("ordinary source must compile to an ordinary plan");
9913        };
9914        let executable_rule_count = plan.rules_by_scc.iter().map(Vec::len).sum::<usize>();
9915        assert_eq!(program.program.facts().count(), 3);
9916        let expected_executable_rules =
9917            program.program.proper_rules().count() + program.program.queries.len();
9918        assert_eq!(
9919            executable_rule_count, expected_executable_rules,
9920            "compiled rules must correspond only to executable source and query rules"
9921        );
9922
9923        provider.reset_host_transfer_stats();
9924        let mut executor = program.prepare_executor(&provider, HashMap::new(), true)?;
9925        let fact_load_transfers = provider.host_transfer_stats();
9926        assert_eq!(fact_load_transfers.htod_calls, 1);
9927        assert_eq!(
9928            fact_load_transfers.htod_bytes,
9929            2 * std::mem::size_of::<u32>() as u64
9930        );
9931        assert_eq!(fact_load_transfers.dtoh_calls, 0);
9932        assert_eq!(fact_load_transfers.dtoh_bytes, 0);
9933        let base = executor
9934            .store()
9935            .get("base")
9936            .ok_or_else(|| XlogError::Execution("missing grouped base facts".to_string()))?;
9937        assert_eq!(base.cached_row_count(), Some(2));
9938        let mut materialized_base = provider.download_column::<u32>(base, 0)?;
9939        materialized_base.sort_unstable();
9940        assert_eq!(materialized_base, vec![1, 2]);
9941
9942        executor.execute_plan(plan)?;
9943        for query_index in 0..2 {
9944            let relation_name = format!("__xlog_query_{query_index}");
9945            let query = executor.store().get(&relation_name).ok_or_else(|| {
9946                XlogError::Execution(format!("missing query relation {relation_name}"))
9947            })?;
9948            let mut rows = provider.download_column::<u32>(query, 0)?;
9949            rows.sort_unstable();
9950            assert_eq!(rows, vec![1, 2]);
9951        }
9952
9953        let stats = executor.execution_stats(4);
9954        let scan_count = stats
9955            .strata
9956            .iter()
9957            .flat_map(|stratum| &stratum.ops)
9958            .filter(|op| op.op_name == "scan")
9959            .count();
9960        let union_count = stats
9961            .strata
9962            .iter()
9963            .flat_map(|stratum| &stratum.ops)
9964            .filter(|op| op.op_name == "union")
9965            .count();
9966        assert_eq!(scan_count, executable_rule_count);
9967        assert_eq!(union_count, executable_rule_count);
9968        Ok(())
9969    }
9970
9971    #[test]
9972    fn ordinary_execution_after_fact_setup_has_no_host_transfers() -> Result<()> {
9973        let Some(provider) = ground_term_encoding_test_provider() else {
9974            return Ok(());
9975        };
9976        let program = LogicProgram::compile(
9977            r#"
9978                pred base(u32).
9979                pred derived(u32).
9980                base(7).
9981                derived(X) :- base(X).
9982                ?- derived(X).
9983            "#,
9984        )?;
9985        let LogicExecutionPlan::Ordinary(plan) = &program.plan else {
9986            panic!("ordinary source must compile to an ordinary plan");
9987        };
9988
9989        let mut executor = program.prepare_executor(&provider, HashMap::new(), false)?;
9990        provider.reset_host_transfer_stats();
9991        provider.reset_d2h_transfer_count();
9992        provider.reset_untracked_metadata_dtoh_count();
9993        provider.reset_deterministic_d2h_violations();
9994        executor.execute_plan(plan)?;
9995
9996        let transfers = provider.host_transfer_stats();
9997        assert_eq!(
9998            transfers.htod_calls, 0,
9999            "execution must not upload host data"
10000        );
10001        assert_eq!(
10002            transfers.htod_bytes, 0,
10003            "execution must not upload host bytes"
10004        );
10005        assert_eq!(
10006            transfers.dtoh_calls, 0,
10007            "execution must not download device data"
10008        );
10009        assert_eq!(
10010            transfers.dtoh_bytes, 0,
10011            "execution must not download device bytes"
10012        );
10013        assert_eq!(provider.d2h_transfer_count(), 0);
10014        assert_eq!(provider.untracked_metadata_dtoh_count(), 0);
10015        assert_eq!(provider.deterministic_d2h_violation_count(), 0);
10016
10017        let query = executor
10018            .store()
10019            .get("__xlog_query_0")
10020            .ok_or_else(|| XlogError::Execution("missing query result".to_string()))?;
10021        assert_eq!(provider.download_column::<u32>(query, 0)?, vec![7]);
10022        Ok(())
10023    }
10024
10025    #[test]
10026    fn ordinary_routes_profile_logical_query_rows() -> Result<()> {
10027        let _lock = resident_env_lock().lock().unwrap();
10028        let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
10029        let Some(provider) = ground_term_encoding_test_provider() else {
10030            return Ok(());
10031        };
10032        let program = LogicProgram::compile(
10033            r#"
10034                pred input(u32).
10035                pred answer(u32).
10036                input(1).
10037                answer(X) :- input(X).
10038                answer(X) :- input(X).
10039                ?- answer(X).
10040            "#,
10041        )?;
10042        assert_eq!(program.plan_kind_label(), "ordinary");
10043
10044        let direct = program.evaluate_with_options(provider.clone(), HashMap::new(), true)?;
10045        let direct_logical =
10046            u64::try_from(provider.validated_logical_row_count(&direct.queries[0].buffer)?)
10047                .map_err(|_| XlogError::Execution("test row count exceeds u64".into()))?;
10048        assert!(direct.queries[0].buffer.num_rows() > direct_logical);
10049        assert_eq!(
10050            direct.stats.as_ref().map(|stats| stats.total_output_rows),
10051            Some(1)
10052        );
10053
10054        let store = program.create_relation_store(provider.clone())?;
10055        let stored = program.evaluate_with_relation_store(provider.clone(), &store, true)?;
10056        assert_eq!(
10057            stored.stats.as_ref().map(|stats| stats.total_output_rows),
10058            Some(1)
10059        );
10060
10061        let mut session = program.create_session_runtime(provider.clone(), &store, true)?;
10062        let (retained, _) = program.evaluate_with_session_runtime(provider, &mut session)?;
10063        assert_eq!(retained.stats.map(|stats| stats.total_output_rows), Some(1));
10064        Ok(())
10065    }
10066
10067    #[test]
10068    fn unprofiled_ordinary_completion_does_not_read_query_row_metadata() -> Result<()> {
10069        let Some(provider) = ground_term_encoding_test_provider() else {
10070            return Ok(());
10071        };
10072        let program = LogicProgram::compile(
10073            r#"
10074                pred input(u32).
10075                pred answer(u32).
10076                input(1).
10077                answer(X) :- input(X).
10078                answer(X) :- input(X).
10079                ?- answer(X).
10080            "#,
10081        )?;
10082
10083        let mut executor = program.prepare_executor(&provider, HashMap::new(), false)?;
10084        executor.execute_plan(program.ordinary_plan("unprofiled completion test")?)?;
10085        let query = executor
10086            .store_mut()
10087            .get_mut("__xlog_query_0")
10088            .expect("compiled query relation");
10089        query.set_row_capacity(query.num_rows());
10090        assert_eq!(query.cached_row_count(), None);
10091        provider.reset_untracked_metadata_dtoh_count();
10092        let result = program.finish_ordinary_evaluation(&provider, executor, false, None, None)?;
10093        assert!(result.stats.is_none());
10094        assert_eq!(provider.untracked_metadata_dtoh_count(), 0);
10095        Ok(())
10096    }
10097
10098    fn assert_profiled_nonordinary_query_rows(
10099        source: &str,
10100        expected_plan_kind: &str,
10101    ) -> Result<()> {
10102        let _lock = resident_env_lock().lock().unwrap();
10103        let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
10104        let Some(provider) = ground_term_encoding_test_provider() else {
10105            return Ok(());
10106        };
10107        let program = LogicProgram::compile(source)?;
10108        assert_eq!(program.plan_kind_label(), expected_plan_kind);
10109
10110        let result = program.evaluate_with_options(provider.clone(), HashMap::new(), true)?;
10111        let row_counts = result
10112            .queries
10113            .iter()
10114            .map(|query| {
10115                Ok::<_, XlogError>((
10116                    query.buffer.num_rows(),
10117                    u64::try_from(provider.validated_logical_row_count(&query.buffer)?)
10118                        .map_err(|_| XlogError::Execution("test row count exceeds u64".into()))?,
10119                ))
10120            })
10121            .collect::<Result<Vec<_>>>()?;
10122        let logical_rows = row_counts.iter().map(|(_, logical)| logical).sum();
10123        assert!(
10124            row_counts
10125                .iter()
10126                .any(|(physical, logical)| physical > logical),
10127            "fixture must distinguish physical capacity from logical rows: {row_counts:?}"
10128        );
10129        assert_eq!(
10130            result.stats.map(|stats| stats.total_output_rows),
10131            Some(logical_rows)
10132        );
10133        Ok(())
10134    }
10135
10136    #[test]
10137    fn wfs_profiles_logical_query_rows_with_capacity_slack() -> Result<()> {
10138        assert_profiled_nonordinary_query_rows(
10139            r#"
10140                #pragma epistemic_mode = faeel
10141                pred vertex(u32).
10142                pred seed(u32, u32).
10143                pred linked(u32, u32).
10144                pred reach(u32, u32).
10145                pred answer(u32, u32).
10146                vertex(1). vertex(2). vertex(3).
10147                seed(1, 2).
10148                reach(X, Y) :- linked(X, Y).
10149                reach(X, Z) :- reach(X, Y), linked(Y, Z).
10150                linked(X, Y) :- vertex(X), vertex(Y), not know reach(X, Y).
10151                linked(X, Y) :- seed(X, Y).
10152                answer(X, Y) :- reach(X, Y).
10153                answer(X, Y) :- reach(X, Y).
10154                ?- answer(X, Y).
10155            "#,
10156            "epistemic_wfs_gpu",
10157        )
10158    }
10159
10160    #[test]
10161    fn g91_profiles_logical_query_rows_with_capacity_slack() -> Result<()> {
10162        assert_profiled_nonordinary_query_rows(
10163            r#"
10164                #pragma epistemic_mode = g91
10165                pred domain(u32).
10166                pred p(u32).
10167                pred q(u32).
10168                pred answer(u32).
10169                domain(7).
10170                p(X) :- domain(X), possible q(X).
10171                q(X) :- domain(X), possible p(X).
10172                answer(X) :- p(X).
10173                answer(X) :- p(X).
10174                ?- answer(X).
10175            "#,
10176            "epistemic_g91_compatibility_gpu",
10177        )
10178    }
10179
10180    #[test]
10181    fn stratified_epistemic_profiles_logical_query_rows() -> Result<()> {
10182        assert_profiled_nonordinary_query_rows(
10183            r#"
10184                #pragma epistemic_mode = faeel
10185                pred node(u32).
10186                pred edge(u32, u32).
10187                pred accepted_edge(u32, u32).
10188                pred accepted_path(u32, u32).
10189                node(1). node(2). node(3).
10190                edge(1, 2). edge(2, 3).
10191                accepted_edge(X, Y) :- node(X), node(Y), know edge(X, Y).
10192                accepted_path(X, Y) :- node(X), node(Y), know accepted_edge(X, Y).
10193            "#,
10194            "epistemic_stratified",
10195        )
10196    }
10197
10198    #[test]
10199    fn grouped_fact_loading_preserves_arity_qualified_relations() -> Result<()> {
10200        let Some(provider) = ground_term_encoding_test_provider() else {
10201            return Ok(());
10202        };
10203        let program = LogicProgram::compile(
10204            r#"
10205                #pragma epistemic_mode = faeel
10206                pred node(symbol).
10207                pred source(symbol, i64).
10208                pred source(u32).
10209                pred result(symbol).
10210                node(key).
10211                source(key, 5000000000).
10212                source(key, 5000000000).
10213                source(1).
10214                source(1).
10215                result(X) :- node(X), know source(X, Y).
10216                ?- result(X).
10217            "#,
10218        )?;
10219        let store = program.create_relation_store(provider.clone())?;
10220        let unary = store
10221            .get("source/1")
10222            .ok_or_else(|| XlogError::Execution("missing unary source facts".to_string()))?;
10223        let binary = store
10224            .get("source/2")
10225            .ok_or_else(|| XlogError::Execution("missing binary source facts".to_string()))?;
10226
10227        assert_eq!(unary.cached_row_count(), Some(1));
10228        assert_eq!(binary.cached_row_count(), Some(1));
10229        assert_eq!(provider.download_column::<u32>(unary, 0)?, vec![1]);
10230        assert_eq!(
10231            provider.download_column::<u32>(binary, 0)?,
10232            vec![symbol::intern("key")]
10233        );
10234        assert_eq!(
10235            provider.download_column::<i64>(binary, 1)?,
10236            vec![5_000_000_000]
10237        );
10238
10239        let evidence = program.execute_epistemic_evidence(provider.clone(), HashMap::new())?;
10240        assert_eq!(
10241            evidence.final_output.schema().arity(),
10242            1,
10243            "the epistemic evidence path must project the public result arity"
10244        );
10245        assert_eq!(
10246            provider.download_column::<u32>(&evidence.final_output, 0)?,
10247            vec![symbol::intern("key")],
10248            "the epistemic evidence path must execute the binary modal source facts"
10249        );
10250        Ok(())
10251    }
10252
10253    #[test]
10254    fn ordinary_compile_qualifies_same_name_multi_arity_relations() -> Result<()> {
10255        let program = LogicProgram::compile(
10256            r#"
10257                pred item(u32). pred item(u32, u32).
10258                pred unary(u32). pred binary(u32, u32).
10259                item(1). item(1, 2).
10260                unary(X) :- item(X).
10261                binary(X, Y) :- item(X, Y).
10262                ?- unary(X).
10263                ?- binary(X, Y).
10264            "#,
10265        )?;
10266
10267        let unary_item = *program
10268            .rel_ids
10269            .get("item/1")
10270            .expect("ordinary compiler registers item/1");
10271        let binary_item = *program
10272            .rel_ids
10273            .get("item/2")
10274            .expect("ordinary compiler registers item/2");
10275        assert_ne!(unary_item, binary_item);
10276
10277        let LogicExecutionPlan::Ordinary(plan) = &program.plan else {
10278            panic!("ordinary source must compile to an ordinary execution plan");
10279        };
10280        let unary_rule = plan
10281            .rules_by_scc
10282            .iter()
10283            .flatten()
10284            .find(|rule| rule.head == "unary")
10285            .expect("compiled unary rule");
10286        let binary_rule = plan
10287            .rules_by_scc
10288            .iter()
10289            .flatten()
10290            .find(|rule| rule.head == "binary")
10291            .expect("compiled binary rule");
10292        assert_eq!(unary_rule.body.referenced_relations(), vec![unary_item]);
10293        assert_eq!(binary_rule.body.referenced_relations(), vec![binary_item]);
10294        Ok(())
10295    }
10296
10297    #[test]
10298    fn recursive_execution_preserves_inline_fact_semantics() -> Result<()> {
10299        let Some(provider) = ground_term_encoding_test_provider() else {
10300            return Ok(());
10301        };
10302        let program = LogicProgram::compile(
10303            r#"
10304                pred edge(u32, u32).
10305                pred reach(u32, u32).
10306                edge(1, 2).
10307                edge(2, 3).
10308                reach(X, Y) :- edge(X, Y).
10309                reach(X, Z) :- reach(X, Y), edge(Y, Z).
10310                ?- reach(X, Z).
10311            "#,
10312        )?;
10313
10314        let result = program.evaluate(provider.clone(), HashMap::new())?;
10315        assert_eq!(result.queries.len(), 1);
10316        let xs = provider.download_column::<u32>(&result.queries[0].buffer, 0)?;
10317        let zs = provider.download_column::<u32>(&result.queries[0].buffer, 1)?;
10318        let mut rows = xs.into_iter().zip(zs).collect::<Vec<_>>();
10319        rows.sort_unstable();
10320        assert_eq!(
10321            rows,
10322            vec![(1, 2), (1, 3), (2, 3)],
10323            "recursive production execution must derive the transitive inline-fact result"
10324        );
10325        Ok(())
10326    }
10327
10328    #[test]
10329    fn nullary_execution_preserves_asserted_inline_fact_truth() -> Result<()> {
10330        let Some(provider) = ground_term_encoding_test_provider() else {
10331            return Ok(());
10332        };
10333        let program = LogicProgram::compile(
10334            r#"
10335                pred enabled().
10336                pred result().
10337                enabled().
10338                result() :- enabled().
10339                ?- result().
10340            "#,
10341        )?;
10342
10343        let result = program.evaluate(provider.clone(), HashMap::new())?;
10344        assert_eq!(result.queries.len(), 1);
10345        assert_eq!(
10346            provider.device_row_count(&result.queries[0].buffer)?,
10347            1,
10348            "an asserted nullary fact must make the derived nullary query true"
10349        );
10350        Ok(())
10351    }
10352
10353    #[test]
10354    fn caller_input_is_unioned_with_inline_facts_before_execution() -> Result<()> {
10355        let Some(provider) = ground_term_encoding_test_provider() else {
10356            return Ok(());
10357        };
10358        let program = LogicProgram::compile(
10359            r#"
10360                pred source(u32).
10361                pred result(u32).
10362                source(1).
10363                result(X) :- source(X).
10364                ?- result(X).
10365            "#,
10366        )?;
10367        let source_schema = program
10368            .schema("source")
10369            .ok_or_else(|| XlogError::Execution("missing source schema".to_string()))?
10370            .clone();
10371        let caller_value = 2u32.to_le_bytes();
10372        let caller_input =
10373            provider.create_buffer_from_slices(&[caller_value.as_slice()], source_schema)?;
10374
10375        let result = program.evaluate(
10376            provider.clone(),
10377            HashMap::from([("source".to_string(), caller_input)]),
10378        )?;
10379        let mut rows = provider.download_column::<u32>(&result.queries[0].buffer, 0)?;
10380        rows.sort_unstable();
10381        assert_eq!(
10382            rows,
10383            vec![1, 2],
10384            "caller-provided rows and inline facts must both reach the executable plan"
10385        );
10386        Ok(())
10387    }
10388
10389    struct RecursiveDuplicateFactProfile {
10390        executable_rule_count: usize,
10391        scan_count: usize,
10392        union_count: usize,
10393        rows: Vec<(u32, u32)>,
10394    }
10395
10396    fn recursive_duplicate_fact_profile(
10397        provider: Arc<CudaKernelProvider>,
10398        fact_count: usize,
10399    ) -> Result<RecursiveDuplicateFactProfile> {
10400        let facts = "edge(1, 2).\n".repeat(fact_count);
10401        let program = LogicProgram::compile(&format!(
10402            r#"
10403                pred edge(u32, u32).
10404                pred reach(u32, u32).
10405                {facts}
10406                reach(X, Y) :- edge(X, Y).
10407                reach(X, Z) :- reach(X, Y), edge(Y, Z).
10408                ?- reach(X, Z).
10409            "#
10410        ))?;
10411        assert_eq!(program.program.facts().count(), fact_count);
10412        assert_eq!(
10413            program.program.proper_rules().count(),
10414            2,
10415            "the recursive rule shape must remain constant across fact counts"
10416        );
10417        let LogicExecutionPlan::Ordinary(plan) = &program.plan else {
10418            panic!("recursive source must compile to an ordinary plan");
10419        };
10420        let executable_rule_count = plan.rules_by_scc.iter().map(Vec::len).sum::<usize>();
10421
10422        let result = program.evaluate_with_options(provider.clone(), HashMap::new(), true)?;
10423        let stats = result
10424            .stats
10425            .as_ref()
10426            .ok_or_else(|| XlogError::Execution("missing execution profile".to_string()))?;
10427        let scan_count = stats
10428            .strata
10429            .iter()
10430            .flat_map(|stratum| &stratum.ops)
10431            .filter(|op| op.op_name == "scan")
10432            .count();
10433        let union_count = stats
10434            .strata
10435            .iter()
10436            .flat_map(|stratum| &stratum.ops)
10437            .filter(|op| op.op_name == "union")
10438            .count();
10439        let xs = provider.download_column::<u32>(&result.queries[0].buffer, 0)?;
10440        let ys = provider.download_column::<u32>(&result.queries[0].buffer, 1)?;
10441        let mut rows = xs.into_iter().zip(ys).collect::<Vec<_>>();
10442        rows.sort_unstable();
10443        Ok(RecursiveDuplicateFactProfile {
10444            executable_rule_count,
10445            scan_count,
10446            union_count,
10447            rows,
10448        })
10449    }
10450
10451    #[test]
10452    fn recursive_plan_operations_are_invariant_to_duplicate_fact_count() -> Result<()> {
10453        let Some(provider) = ground_term_encoding_test_provider() else {
10454            return Ok(());
10455        };
10456
10457        let one_fact = recursive_duplicate_fact_profile(provider.clone(), 1)?;
10458        let many_facts = recursive_duplicate_fact_profile(provider, 64)?;
10459        assert_eq!(one_fact.rows, vec![(1, 2)]);
10460        assert_eq!(many_facts.rows, one_fact.rows);
10461        assert_eq!(
10462            many_facts.executable_rule_count, one_fact.executable_rule_count,
10463            "executable rule count must not scale with source fact count"
10464        );
10465        assert_eq!(
10466            many_facts.scan_count, one_fact.scan_count,
10467            "executable scan count must not scale with source fact count"
10468        );
10469        assert_eq!(
10470            many_facts.union_count, one_fact.union_count,
10471            "executable union count must not scale with source fact count"
10472        );
10473        Ok(())
10474    }
10475
10476    #[test]
10477    fn g91_compatibility_plan_records_gpu_upper_and_refinement_passes() -> Result<()> {
10478        let program = LogicProgram::compile(
10479            r#"
10480                #pragma epistemic_mode = g91
10481                pred domain(u32).
10482                pred p(u32).
10483                pred q(u32).
10484                domain(7).
10485                p(X) :- domain(X), possible q(X).
10486                q(X) :- domain(X), possible p(X).
10487                ?- p(X).
10488                ?- q(X).
10489            "#,
10490        )?;
10491
10492        let LogicExecutionPlan::EpistemicG91Compatibility(plan) = &program.plan else {
10493            panic!("mutual G91 possibility cycle must select compatibility iteration");
10494        };
10495        assert_eq!(plan.snapshot_relations.len(), 2);
10496        assert_eq!(plan.convergence_predicates, vec!["p", "q"]);
10497        let summary = program
10498            .epistemic_plan_json()
10499            .expect("epistemic compatibility summary");
10500        assert!(summary.contains("epistemic_g91_compatibility_gpu"));
10501        assert!(summary.contains("\"gpu_passes\":[\"upper_bound\",\"refinement\"]"));
10502        assert!(summary.contains("\"execution_backend\":\"gpu\""));
10503        assert!(summary.contains("\"fallback_policy\":\"reject_unsupported\""));
10504        assert!(!summary.contains("\"cpu_fallback_total_zero\""));
10505        assert!(!summary.contains("\"cpu_fallback_is_zero\""));
10506        Ok(())
10507    }
10508
10509    #[test]
10510    fn g91_compatibility_infers_undeclared_snapshot_schema() -> Result<()> {
10511        let program = LogicProgram::compile(
10512            r#"
10513                #pragma epistemic_mode = g91
10514                domain(1).
10515                p(X) :- domain(X), possible p(X).
10516                ?- p(X).
10517            "#,
10518        )?;
10519
10520        let LogicExecutionPlan::EpistemicG91Compatibility(plan) = &program.plan else {
10521            panic!("undeclared G91 relation must select compatibility iteration");
10522        };
10523        let snapshot = plan
10524            .snapshot_relations
10525            .get("p")
10526            .expect("snapshot name for p");
10527        let refinement_schemas = gpu_evaluation_pass_schemas(&plan.refinement);
10528        let schema = refinement_schemas
10529            .get(snapshot)
10530            .expect("inferred snapshot schema");
10531        assert_eq!(schema.arity(), 1);
10532        assert_eq!(schema.column_type(0), Some(ScalarType::U32));
10533        Ok(())
10534    }
10535
10536    #[test]
10537    fn g91_compatibility_compile_rejects_a_recursive_aggregate_component() {
10538        let result = LogicProgram::compile(
10539            r#"
10540                #pragma epistemic_mode = g91
10541                pred seed(u32).
10542                pred p(u32).
10543                pred totals(u64).
10544                seed(1).
10545                p(X) :- seed(X), possible p(X).
10546                p(X) :- seed(X), totals(_).
10547                totals(count(X)) :- p(X).
10548                ?- p(X).
10549            "#,
10550        );
10551        let error = match result {
10552            Ok(_) => panic!("recursive aggregation must not enter compatibility refinement"),
10553            Err(error) => error,
10554        };
10555        let message = error.to_string();
10556        assert!(message.contains("Gelfond-1991 compatibility"), "{message}");
10557        assert!(message.contains("aggregate"), "{message}");
10558        assert!(message.contains("totals"), "{message}");
10559    }
10560
10561    #[test]
10562    fn g91_introspection_preserves_authored_rules_and_queries() -> Result<()> {
10563        let program = LogicProgram::compile(
10564            r#"
10565                #pragma epistemic_mode = g91
10566                pred base(u32).
10567                pred p(u32).
10568                base(1).
10569                p(X) :- base(X), possible p(X).
10570                ?- p(X).
10571            "#,
10572        )?;
10573
10574        let provenance = program.rule_provenance();
10575        let p_rule = provenance
10576            .iter()
10577            .find(|rule| rule.head == "p(X)")
10578            .expect("authored p rule provenance");
10579        assert_eq!(p_rule.support_relation_ids, vec!["base", "p"]);
10580        assert!(provenance.iter().all(|rule| {
10581            rule.support_relation_ids
10582                .iter()
10583                .all(|relation| !relation.starts_with("__xlog_"))
10584        }));
10585
10586        let traces = program.proof_traces();
10587        assert_eq!(traces.len(), 1);
10588        assert_eq!(traces[0].query, "p(X)");
10589        assert!(traces[0].source_facts.iter().any(|fact| fact == "base(1)."));
10590        Ok(())
10591    }
10592
10593    #[test]
10594    fn predicate_function_proof_traces_preserve_source_names_and_normalized_support() -> Result<()>
10595    {
10596        let program = LogicProgram::compile(
10597            r#"
10598                pred candidate(i32, i32).
10599                pred blocked(i32).
10600                pred forbidden(i32).
10601                pred answer(i32).
10602                candidate(1, 2).
10603                func visible(X) = Y :- candidate(X, Y), not blocked(Y).
10604                answer(Y) :- Y is visible(1), not forbidden(Y).
10605                ?- answer(Y).
10606            "#,
10607        )?;
10608
10609        let provenance = program.rule_provenance();
10610        let answer_rule = provenance
10611            .iter()
10612            .find(|rule| rule.head == "answer(Y)")
10613            .expect("answer rule provenance");
10614        assert!(
10615            ["candidate", "blocked", "forbidden"]
10616                .iter()
10617                .all(|relation| answer_rule
10618                    .support_relation_ids
10619                    .iter()
10620                    .any(|id| id == relation)),
10621            "{:?}",
10622            answer_rule.support_relation_ids
10623        );
10624
10625        let traces = program.proof_traces();
10626        assert_eq!(traces.len(), 1);
10627        assert!(traces[0]
10628            .rejected_alternatives
10629            .iter()
10630            .any(|alternative| alternative == "not blocked(Y)"));
10631        assert!(traces[0]
10632            .rejected_alternatives
10633            .iter()
10634            .any(|alternative| alternative == "not forbidden(Y)"));
10635        assert!(traces[0]
10636            .source_facts
10637            .iter()
10638            .any(|fact| fact == "candidate(1, 2)."));
10639        assert!(!format!("{provenance:?}{traces:?}").contains("__XLOG_FUNCTION"));
10640        Ok(())
10641    }
10642
10643    #[test]
10644    fn relation_clone_context_preserves_cuda_error_variants() {
10645        let kernel = relation_clone_error(
10646            "cloning relation 'fact'".to_string(),
10647            XlogError::Kernel("launch failed".to_string()),
10648        );
10649        assert!(matches!(
10650            kernel,
10651            XlogError::Kernel(message)
10652                if message == "cloning relation 'fact': launch failed"
10653        ));
10654
10655        let exhausted = relation_clone_error(
10656            "cloning relation 'fact'".to_string(),
10657            XlogError::ResourceExhausted {
10658                context: "GPU memory pressure: layer=manager_alloc current_bytes=60 requested_bytes=4 required_bytes=64 required_u64_overflow=false budget_bytes=63 prior_peak_bytes=60".to_string(),
10659                estimated_bytes: 64,
10660                budget_bytes: 63,
10661            },
10662        );
10663        match exhausted {
10664            XlogError::ResourceExhausted {
10665                context,
10666                estimated_bytes,
10667                budget_bytes,
10668            } => {
10669                assert_eq!(
10670                    context,
10671                    "cloning relation 'fact': GPU memory pressure: layer=manager_alloc current_bytes=60 requested_bytes=4 required_bytes=64 required_u64_overflow=false budget_bytes=63 prior_peak_bytes=60"
10672                );
10673                assert_eq!(estimated_bytes, 64);
10674                assert_eq!(budget_bytes, 63);
10675            }
10676            error => panic!("expected resource exhaustion, got {error}"),
10677        }
10678    }
10679
10680    #[test]
10681    fn compiled_argument_preserves_declared_names_domains_and_order() -> Result<()> {
10682        let program = LogicProgram::compile(
10683            r#"
10684                domain party: u32.
10685                pred transfer(giver: party, receiver: party, asset: u32, time: i64).
10686                pred positional(u32, i64, symbol).
10687            "#,
10688        )?;
10689
10690        let transfer = program
10691            .argument_schema("transfer")
10692            .expect("compiled transfer argument schema");
10693        assert_eq!(
10694            transfer
10695                .iter()
10696                .map(|argument| (
10697                    argument.name(),
10698                    argument.source_named(),
10699                    argument.sort(),
10700                    argument.scalar_type(),
10701                ))
10702                .collect::<Vec<_>>(),
10703            vec![
10704                ("giver", true, Some("party"), ScalarType::U32),
10705                ("receiver", true, Some("party"), ScalarType::U32),
10706                ("asset", true, None, ScalarType::U32),
10707                ("time", true, None, ScalarType::I64),
10708            ]
10709        );
10710
10711        let positional = program
10712            .argument_schema("positional")
10713            .expect("compiled positional argument schema");
10714        assert_eq!(
10715            positional
10716                .iter()
10717                .map(|argument| (
10718                    argument.name(),
10719                    argument.source_named(),
10720                    argument.sort(),
10721                    argument.scalar_type(),
10722                ))
10723                .collect::<Vec<_>>(),
10724            vec![
10725                ("c0", false, None, ScalarType::U32),
10726                ("c1", false, None, ScalarType::I64),
10727                ("c2", false, None, ScalarType::Symbol),
10728            ]
10729        );
10730
10731        Ok(())
10732    }
10733
10734    #[test]
10735    fn compiled_argument_uses_schema_metadata_for_inferred_relations() -> Result<()> {
10736        let program = LogicProgram::compile(
10737            r#"
10738                pred source(value: i64).
10739                inferred(X) :- source(X).
10740            "#,
10741        )?;
10742
10743        let inferred = program
10744            .argument_schema("inferred")
10745            .expect("compiled inferred argument schema");
10746        assert_eq!(
10747            inferred
10748                .iter()
10749                .map(|argument| (
10750                    argument.name(),
10751                    argument.source_named(),
10752                    argument.sort(),
10753                    argument.scalar_type(),
10754                ))
10755                .collect::<Vec<_>>(),
10756            vec![("c0", false, None, ScalarType::I64)]
10757        );
10758        assert!(program.argument_schema("unknown").is_none());
10759
10760        Ok(())
10761    }
10762
10763    #[test]
10764    fn compiled_argument_recovers_source_metadata_for_arity_qualified_relations() -> Result<()> {
10765        let program = LogicProgram::compile(
10766            r#"
10767                domain identity: u32.
10768                pred joined(u32, u32).
10769                pred seed(u32, u32).
10770                pred polymorphic(value: identity).
10771                pred polymorphic(left: identity, right: identity).
10772
10773                joined(X, Y) :- seed(X, Y), know polymorphic(X), possible polymorphic(X, Y).
10774            "#,
10775        )?;
10776
10777        let unary = program
10778            .argument_schema("polymorphic/1")
10779            .expect("compiled arity-qualified argument schema");
10780        assert_eq!(
10781            unary
10782                .iter()
10783                .map(|argument| (
10784                    argument.name(),
10785                    argument.source_named(),
10786                    argument.sort(),
10787                    argument.scalar_type(),
10788                ))
10789                .collect::<Vec<_>>(),
10790            vec![("value", true, Some("identity"), ScalarType::U32)]
10791        );
10792
10793        Ok(())
10794    }
10795
10796    #[test]
10797    fn epistemic_compile_qualifies_extensional_signatures_used_by_reduction() -> Result<()> {
10798        let program = LogicProgram::compile(
10799            r#"
10800                #pragma epistemic_mode = faeel
10801                pred node(symbol).
10802                pred source(symbol, i64).
10803                pred source(u32).
10804                pred result(symbol).
10805                node(key).
10806                source(key, 5000000000).
10807                source(1).
10808                result(X) :- node(X), know source(X, Y).
10809                ?- result(X).
10810            "#,
10811        )?;
10812
10813        let source = program
10814            .schema("source/2")
10815            .expect("missing arity-qualified binary source schema");
10816        assert_eq!(source.column_type(0), Some(ScalarType::Symbol));
10817        assert_eq!(source.column_type(1), Some(ScalarType::I64));
10818        assert!(program.schema("source/1").is_some());
10819        assert_eq!(
10820            program
10821                .schema("result")
10822                .expect("missing augmented result schema")
10823                .arity(),
10824            2
10825        );
10826
10827        Ok(())
10828    }
10829
10830    #[test]
10831    fn epistemic_compile_infers_hidden_column_types_from_runtime_binders() -> Result<()> {
10832        let inferred_source = LogicProgram::compile(
10833            r#"
10834                #pragma epistemic_mode = faeel
10835                pred node(symbol).
10836                pred result(symbol).
10837                node(key).
10838                raw(key, 5000000000).
10839                edge(X, Y) :- raw(X, Y).
10840                result(X) :- node(X), know edge(X, Y).
10841                ?- result(X).
10842            "#,
10843        )?;
10844        assert_eq!(
10845            inferred_source
10846                .schema("result")
10847                .expect("missing inferred-source result schema")
10848                .column_type(1),
10849            Some(ScalarType::I64)
10850        );
10851
10852        let arithmetic_binding = LogicProgram::compile(
10853            r#"
10854                #pragma epistemic_mode = faeel
10855                pred node(symbol).
10856                pred allowed(u64).
10857                pred result(symbol).
10858                node(key).
10859                allowed(1).
10860                result(X) :- node(X), Y is cast(1, u64), not know allowed(Y).
10861                ?- result(X).
10862            "#,
10863        )?;
10864        assert_eq!(
10865            arithmetic_binding
10866                .schema("result")
10867                .expect("missing arithmetic-bound result schema")
10868                .column_type(1),
10869            Some(ScalarType::U64)
10870        );
10871
10872        Ok(())
10873    }
10874
10875    #[test]
10876    fn epistemic_compile_uses_one_extensional_arity_census() -> Result<()> {
10877        let program = LogicProgram::compile(
10878            r#"
10879                #pragma epistemic_mode = faeel
10880                p(a).
10881                result(X) :- p(X), know p(X).
10882                :- p(X, Y).
10883                ?- result(X).
10884            "#,
10885        )?;
10886
10887        assert!(program.schema("p/1").is_some());
10888        assert!(program.schema("p/2").is_some());
10889        Ok(())
10890    }
10891
10892    #[test]
10893    fn epistemic_compile_preserves_recursive_stratum_schema() -> Result<()> {
10894        let program = LogicProgram::compile(
10895            r#"
10896                #pragma epistemic_mode = faeel
10897                pred node(u32).
10898                pred edge(u32, u32).
10899                pred accepted_edge(u32, u32).
10900                pred reach(u32, u32).
10901                node(1).
10902                node(2).
10903                node(3).
10904                edge(1, 2).
10905                edge(2, 3).
10906                accepted_edge(X, Y) :- node(X), node(Y), know edge(X, Y).
10907                reach(X, Y) :- node(X), node(Y), know accepted_edge(X, Y).
10908                reach(X, Z) :- reach(X, Y), node(Z), know accepted_edge(Y, Z).
10909                ?- reach(X, Z).
10910            "#,
10911        )?;
10912
10913        assert_eq!(
10914            program
10915                .schema("reach")
10916                .expect("missing recursive reach schema")
10917                .arity(),
10918            2
10919        );
10920        assert_eq!(program.plan_kind_label(), "epistemic_stratified");
10921        Ok(())
10922    }
10923
10924    #[test]
10925    fn epistemic_compile_rejects_unscoped_same_head_rule_unions() {
10926        let error = LogicProgram::compile(
10927            r#"
10928                #pragma epistemic_mode = faeel
10929                pred p().
10930                pred q().
10931                pred result(symbol).
10932                q().
10933                result(a) :- know p().
10934                result(b) :- know q().
10935                ?- result(X).
10936            "#,
10937        )
10938        .err()
10939        .expect("same-head modal clauses require per-clause provenance");
10940        let message = error.to_string();
10941        assert!(message.contains("epistemic rule-union materialization"));
10942        assert!(message.contains("result/1"), "{message}");
10943    }
10944
10945    #[test]
10946    fn epistemic_compile_rejects_derived_source_arity_collisions() {
10947        let sources = [
10948            r#"
10949                #pragma epistemic_mode = faeel
10950                unary(a).
10951                binary(a, b).
10952                result(X) :- unary(X), know unary(X).
10953                result(X, Y) :- binary(X, Y), know binary(X, Y).
10954            "#,
10955            r#"
10956                #pragma epistemic_mode = faeel
10957                node(key).
10958                edge(key, 5000000000).
10959                result(X) :- node(X), know edge(X, Y).
10960                ?- result(A, B).
10961            "#,
10962        ];
10963
10964        for source in sources {
10965            let error = match LogicProgram::compile(source) {
10966                Ok(_) => panic!("derived source-arity collisions must fail compilation"),
10967                Err(error) => error,
10968            };
10969            assert!(
10970                matches!(
10971                    &error,
10972                    XlogError::UnsupportedEpistemicConstruct { construct, .. }
10973                        if construct == "epistemic derived predicate schema"
10974                ),
10975                "{error}"
10976            );
10977        }
10978    }
10979
10980    #[test]
10981    fn epistemic_compile_rejects_constrained_augmented_head_query() {
10982        let error = match LogicProgram::compile(
10983            r#"
10984                #pragma epistemic_mode = faeel
10985                node(key).
10986                edge(key, 5000000000).
10987                result(X) :- node(X), know edge(X, Y).
10988                ?- result(other).
10989            "#,
10990        ) {
10991            Ok(_) => panic!("a constrained augmented-head query must fail compilation"),
10992            Err(error) => error,
10993        };
10994
10995        assert!(
10996            matches!(
10997                &error,
10998                XlogError::UnsupportedEpistemicConstruct { construct, .. }
10999                    if construct == "epistemic augmented head query"
11000            ),
11001            "{error}"
11002        );
11003    }
11004
11005    #[test]
11006    fn epistemic_compile_rejects_divergent_ordinary_bound_internal_arities() {
11007        let error = match LogicProgram::compile(
11008            r#"
11009                #pragma epistemic_mode = faeel
11010                pred node(symbol).
11011                pred edge(symbol, i64).
11012                pred allowed(i64).
11013                pred result(symbol).
11014                node(key).
11015                edge(key, 5000000000).
11016                allowed(5000000000).
11017                result(X) :- node(X).
11018                result(X) :- node(X), edge(X, Y), know allowed(Y).
11019                ?- result(X).
11020            "#,
11021        ) {
11022            Ok(_) => panic!("divergent internal arities must fail compilation"),
11023            Err(error) => error,
11024        };
11025
11026        assert!(
11027            matches!(
11028                &error,
11029                XlogError::UnsupportedEpistemicConstruct { construct, .. }
11030                    if construct == "epistemic augmented predicate schema"
11031            ),
11032            "{error}"
11033        );
11034    }
11035
11036    #[test]
11037    fn compiled_argument_preserves_declared_prefix_when_schema_is_wider() -> Result<()> {
11038        let program = LogicProgram::compile(
11039            r#"
11040                domain identity: u32.
11041                pred node(id: identity).
11042                pred edge(source: identity, target: identity).
11043                pred one_hop(node: identity).
11044
11045                one_hop(X) :- node(X), know edge(X, Y).
11046            "#,
11047        )?;
11048
11049        let one_hop = program
11050            .argument_schema("one_hop")
11051            .expect("compiled widened argument schema");
11052        assert_eq!(
11053            one_hop
11054                .iter()
11055                .map(|argument| (
11056                    argument.name(),
11057                    argument.source_named(),
11058                    argument.sort(),
11059                    argument.scalar_type(),
11060                ))
11061                .collect::<Vec<_>>(),
11062            vec![
11063                ("node", true, Some("identity"), ScalarType::U32),
11064                ("c1", false, None, ScalarType::U32),
11065            ]
11066        );
11067
11068        Ok(())
11069    }
11070
11071    #[test]
11072    fn compiled_argument_uses_the_declaration_selected_by_compilation() -> Result<()> {
11073        let program = LogicProgram::compile(
11074            r#"
11075                domain first: u32.
11076                domain second: i64.
11077                pred duplicate(value: first).
11078                pred duplicate(value: second).
11079            "#,
11080        )?;
11081
11082        let duplicate = program
11083            .argument_schema("duplicate")
11084            .expect("compiled duplicate argument schema");
11085        assert_eq!(
11086            duplicate
11087                .iter()
11088                .map(|argument| (
11089                    argument.name(),
11090                    argument.source_named(),
11091                    argument.sort(),
11092                    argument.scalar_type(),
11093                ))
11094                .collect::<Vec<_>>(),
11095            vec![("value", true, Some("second"), ScalarType::I64)]
11096        );
11097
11098        Ok(())
11099    }
11100}
11101
11102#[cfg(test)]
11103mod relation_delta_coalesce_tests {
11104    use super::*;
11105    use std::collections::HashMap;
11106    use std::sync::Arc;
11107
11108    use xlog_core::{MemoryBudget, ScalarType};
11109
11110    fn test_provider() -> Option<Arc<CudaKernelProvider>> {
11111        let provider = (|| -> Result<Arc<CudaKernelProvider>> {
11112            Ok(Arc::new(
11113                xlog_cuda::CudaProviderBuilder::new(
11114                    0,
11115                    MemoryBudget::with_limit(1024 * 1024 * 1024),
11116                )
11117                .build()?,
11118            ))
11119        })();
11120
11121        finish_test_provider_setup(
11122            provider,
11123            std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1"),
11124        )
11125    }
11126
11127    #[test]
11128    #[should_panic(expected = "XLOG_REQUIRE_CUDA=1 but CUDA provider construction failed")]
11129    fn required_cuda_provider_failure_is_not_silently_skipped() {
11130        finish_test_provider_setup::<()>(
11131            Err(XlogError::Execution("forced provider failure".to_string())),
11132            true,
11133        );
11134    }
11135
11136    fn test_buffer(provider: &CudaKernelProvider, rows: &[u32]) -> CudaBuffer {
11137        let schema = Schema::new(vec![("id".to_string(), ScalarType::U32)]);
11138        let bytes: Vec<u8> = rows.iter().flat_map(|v| v.to_le_bytes()).collect();
11139        let mut col = provider.memory().alloc::<u8>(bytes.len()).expect("alloc");
11140        provider
11141            .device()
11142            .inner()
11143            .htod_sync_copy_into(&bytes, &mut col)
11144            .expect("upload rows");
11145        let mut d_num_rows = provider.memory().alloc::<u32>(1).expect("alloc rows");
11146        let row_count = rows.len() as u32;
11147        provider
11148            .device()
11149            .inner()
11150            .htod_sync_copy_into(&[row_count], &mut d_num_rows)
11151            .expect("upload row count");
11152        CudaBuffer::from_columns(vec![col.into()], rows.len() as u64, d_num_rows, schema)
11153    }
11154
11155    fn read_u32(provider: &CudaKernelProvider, buffer: &CudaBuffer) -> Vec<u32> {
11156        provider
11157            .download_column::<u32>(buffer, 0)
11158            .expect("download")
11159    }
11160
11161    fn sorted_query_rows(provider: &CudaKernelProvider, result: &LogicEvalResult) -> Vec<u32> {
11162        let mut rows = read_u32(provider, &result.queries[0].buffer);
11163        rows.sort_unstable();
11164        rows
11165    }
11166
11167    fn assert_empty_modal_cycle_query_result(
11168        provider: &CudaKernelProvider,
11169        result: &LogicEvalResult,
11170    ) -> Result<()> {
11171        assert_eq!(result.queries.len(), 1);
11172        let query = &result.queries[0];
11173        assert_eq!(query.relation_name, "p");
11174        assert!(query.columns.is_empty());
11175        assert_eq!(query.buffer.schema().arity(), 0);
11176        assert_eq!(provider.device_row_count(&query.buffer)?, 0);
11177        Ok(())
11178    }
11179
11180    #[test]
11181    fn modal_cycle_query_presentation_is_consistent_across_evaluation_apis() -> Result<()> {
11182        let Some(provider) = test_provider() else {
11183            return Ok(());
11184        };
11185        let program = LogicProgram::compile(
11186            r#"
11187                #pragma epistemic_mode = faeel
11188                pred p().
11189                p() :- possible p().
11190                ?- p().
11191            "#,
11192        )?;
11193
11194        let direct = program.evaluate(provider.clone(), HashMap::new())?;
11195        assert_empty_modal_cycle_query_result(provider.as_ref(), &direct)?;
11196
11197        let relation_store = program.create_relation_store(provider.clone())?;
11198        let (from_store, cached_store) = program.evaluate_with_relation_store_and_cache(
11199            provider.clone(),
11200            &relation_store,
11201            false,
11202        )?;
11203        assert_empty_modal_cycle_query_result(provider.as_ref(), &from_store)?;
11204
11205        let cached = program.evaluate_cached_relation_store(provider.clone(), &cached_store)?;
11206        assert_empty_modal_cycle_query_result(provider.as_ref(), &cached)?;
11207
11208        let mut runtime =
11209            program.create_session_runtime(provider.clone(), &relation_store, false)?;
11210        let (from_session, _) =
11211            program.evaluate_with_session_runtime(provider.clone(), &mut runtime)?;
11212        assert_empty_modal_cycle_query_result(provider.as_ref(), &from_session)?;
11213        Ok(())
11214    }
11215
11216    #[test]
11217    fn coalesce_batch_cancels_insert_delete_pairs_on_device() {
11218        let provider = match test_provider() {
11219            Some(provider) => provider,
11220            None => {
11221                eprintln!("Skipping test: no CUDA device available");
11222                return;
11223            }
11224        };
11225
11226        let batch = vec![
11227            (
11228                "streamed_fact".to_string(),
11229                RelationDelta::new(Some(test_buffer(&provider, &[7, 8])), None),
11230            ),
11231            (
11232                "streamed_fact".to_string(),
11233                RelationDelta::new(None, Some(test_buffer(&provider, &[8]))),
11234            ),
11235            (
11236                "streamed_fact".to_string(),
11237                RelationDelta::new(Some(test_buffer(&provider, &[9])), None),
11238            ),
11239        ];
11240
11241        let report = coalesce_relation_delta_batch_with_cancellation_capture(
11242            provider.as_ref(),
11243            batch,
11244            &BTreeSet::new(),
11245        )
11246        .expect("coalesce relation delta batch");
11247        let delta = report
11248            .deltas
11249            .get("streamed_fact")
11250            .expect("coalesced relation");
11251        let insert = delta.insert.as_ref().expect("coalesced insert");
11252        assert_eq!(read_u32(&provider, insert), vec![7, 9]);
11253        assert!(delta.delete.as_ref().map(|b| b.is_empty()).unwrap_or(true));
11254        assert_eq!(report.report_seed.input_delta_count, 3);
11255        assert_eq!(report.report_seed.changed_relations, 1);
11256        assert_eq!(report.report_seed.coalesced_insert_rows, 2);
11257        assert_eq!(report.report_seed.coalesced_delete_rows, 0);
11258        assert_eq!(report.report_seed.canceled_rows, 1);
11259    }
11260
11261    #[test]
11262    fn relation_delta_batch_updates_runtime_store_and_reports_coalesced_counts() -> Result<()> {
11263        let Some(provider) = test_provider() else {
11264            eprintln!("Skipping test: no CUDA device available");
11265            return Ok(());
11266        };
11267
11268        let source = r#"
11269            pred streamed_fact(u32).
11270            pred out(u32).
11271
11272            out(X) :- streamed_fact(X).
11273
11274            ?- out(X).
11275        "#;
11276        let program = LogicProgram::compile(source)?;
11277        let mut coalesced_store = program.create_relation_store(provider.clone())?;
11278        let mut coalesced_cache = None;
11279
11280        provider.reset_host_transfer_stats();
11281        provider.reset_d2h_transfer_count();
11282        let report = program.apply_relation_delta_batch(
11283            provider.clone(),
11284            &mut coalesced_store,
11285            &mut coalesced_cache,
11286            vec![
11287                (
11288                    "streamed_fact".to_string(),
11289                    RelationDelta::new(Some(test_buffer(&provider, &[1, 2, 3])), None),
11290                ),
11291                (
11292                    "streamed_fact".to_string(),
11293                    RelationDelta::new(None, Some(test_buffer(&provider, &[2]))),
11294                ),
11295                (
11296                    "streamed_fact".to_string(),
11297                    RelationDelta::new(Some(test_buffer(&provider, &[4])), None),
11298                ),
11299            ],
11300        )?;
11301        let transfer_stats = provider.host_transfer_stats();
11302
11303        assert_eq!(report.input_delta_count, 3);
11304        assert_eq!(report.changed_relations, 1);
11305        assert_eq!(report.insert_rows, 3);
11306        assert_eq!(report.delete_rows, 0);
11307        assert_eq!(report.coalesced_insert_rows, 3);
11308        assert_eq!(report.coalesced_delete_rows, 0);
11309        assert_eq!(report.canceled_rows, 1);
11310        assert_eq!(transfer_stats.dtoh_bytes, 0);
11311        assert_eq!(transfer_stats.dtoh_calls, 0);
11312        assert_eq!(provider.d2h_transfer_count(), 0);
11313
11314        let coalesced = program.evaluate_cached_relation_store(
11315            provider.clone(),
11316            coalesced_cache
11317                .as_ref()
11318                .expect("cached store after delta batch"),
11319        )?;
11320        let coalesced_rows = sorted_query_rows(&provider, &coalesced);
11321
11322        let mut sequential_store = program.create_relation_store(provider.clone())?;
11323        let mut sequential_cache = None;
11324        for delta in [
11325            RelationDelta::new(Some(test_buffer(&provider, &[1, 2, 3])), None),
11326            RelationDelta::new(None, Some(test_buffer(&provider, &[2]))),
11327            RelationDelta::new(Some(test_buffer(&provider, &[4])), None),
11328        ] {
11329            program.apply_relation_deltas(
11330                provider.clone(),
11331                &mut sequential_store,
11332                &mut sequential_cache,
11333                HashMap::from([("streamed_fact".to_string(), delta)]),
11334            )?;
11335        }
11336        let sequential = program.evaluate_cached_relation_store(
11337            provider.clone(),
11338            sequential_cache
11339                .as_ref()
11340                .expect("cached store after sequential deltas"),
11341        )?;
11342        let sequential_rows = sorted_query_rows(&provider, &sequential);
11343
11344        let mut replacement_store = program.create_relation_store(provider.clone())?;
11345        replacement_store.put("streamed_fact", test_buffer(&provider, &[1, 3, 4]));
11346        let replacement =
11347            program.evaluate_with_relation_store(provider.clone(), &replacement_store, false)?;
11348        let replacement_rows = sorted_query_rows(&provider, &replacement);
11349
11350        assert_eq!(coalesced_rows, vec![1, 3, 4]);
11351        assert_eq!(coalesced_rows, sequential_rows);
11352        assert_eq!(coalesced_rows, replacement_rows);
11353        Ok(())
11354    }
11355}
11356
11357#[cfg(test)]
11358mod relation_delta_preparation_tests {
11359    use super::*;
11360    use std::sync::Arc;
11361
11362    use xlog_core::{MemoryBudget, ScalarType};
11363
11364    fn test_provider_with_budget(limit: u64) -> Option<Arc<CudaKernelProvider>> {
11365        let provider = (|| -> Result<Arc<CudaKernelProvider>> {
11366            Ok(Arc::new(
11367                xlog_cuda::CudaProviderBuilder::new(0, MemoryBudget::with_limit(limit)).build()?,
11368            ))
11369        })();
11370
11371        match provider {
11372            Ok(provider) => Some(provider),
11373            Err(error) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
11374                panic!(
11375                    "XLOG_REQUIRE_CUDA=1 but CUDA provider construction failed: {}",
11376                    error
11377                )
11378            }
11379            Err(error) => {
11380                eprintln!("Skipping test: no CUDA device available ({error})");
11381                None
11382            }
11383        }
11384    }
11385
11386    fn test_provider() -> Option<Arc<CudaKernelProvider>> {
11387        test_provider_with_budget(1024 * 1024 * 1024)
11388    }
11389
11390    fn test_buffer(provider: &CudaKernelProvider, rows: &[u32]) -> CudaBuffer {
11391        let schema = Schema::new(vec![("id".to_string(), ScalarType::U32)]);
11392        let bytes: Vec<u8> = rows.iter().flat_map(|value| value.to_le_bytes()).collect();
11393        let mut column = provider.memory().alloc::<u8>(bytes.len()).expect("alloc");
11394        provider
11395            .device()
11396            .inner()
11397            .htod_sync_copy_into(&bytes, &mut column)
11398            .expect("upload rows");
11399        let mut device_row_count = provider.memory().alloc::<u32>(1).expect("alloc rows");
11400        provider
11401            .device()
11402            .inner()
11403            .htod_sync_copy_into(&[rows.len() as u32], &mut device_row_count)
11404            .expect("upload row count");
11405        CudaBuffer::from_columns(
11406            vec![column.into()],
11407            rows.len() as u64,
11408            device_row_count,
11409            schema,
11410        )
11411    }
11412
11413    fn sorted_u32(provider: &CudaKernelProvider, buffer: &CudaBuffer) -> Vec<u32> {
11414        let mut rows = provider
11415            .download_column::<u32>(buffer, 0)
11416            .expect("download rows");
11417        rows.sort_unstable();
11418        rows
11419    }
11420
11421    fn insert_rows(
11422        provider: &CudaKernelProvider,
11423        prepared: &PreparedRelationDeltaBatch,
11424        relation: &str,
11425    ) -> Vec<u32> {
11426        let delta = prepared
11427            .net_deltas()
11428            .get(relation)
11429            .expect("prepared relation delta");
11430        sorted_u32(
11431            provider,
11432            delta.insert.as_ref().expect("prepared net insert buffer"),
11433        )
11434    }
11435
11436    fn cancellation_batch(provider: &CudaKernelProvider) -> Vec<(String, RelationDelta)> {
11437        vec![
11438            (
11439                "fact".to_string(),
11440                RelationDelta::new(Some(test_buffer(provider, &[5])), None),
11441            ),
11442            (
11443                "fact".to_string(),
11444                RelationDelta::new(None, Some(test_buffer(provider, &[5]))),
11445            ),
11446            (
11447                "fact".to_string(),
11448                RelationDelta::new(Some(test_buffer(provider, &[6])), None),
11449            ),
11450        ]
11451    }
11452
11453    fn report_counts(report: &LogicDeltaReport) -> (usize, usize, u64, u64, u64) {
11454        (
11455            report.input_delta_count,
11456            report.changed_relations,
11457            report.coalesced_insert_rows,
11458            report.coalesced_delete_rows,
11459            report.canceled_rows,
11460        )
11461    }
11462
11463    #[test]
11464    fn prepared_batch_exposes_net_deltas_and_ordered_cancellation_buffers() -> Result<()> {
11465        let Some(provider) = test_provider() else {
11466            return Ok(());
11467        };
11468        let program = LogicProgram::compile("pred fact(u32).")?;
11469
11470        let prepared = program.prepare_relation_delta_batch(
11471            provider.as_ref(),
11472            vec![
11473                (
11474                    "fact".to_string(),
11475                    RelationDelta::new(Some(test_buffer(&provider, &[1, 2])), None),
11476                ),
11477                (
11478                    "fact".to_string(),
11479                    RelationDelta::new(None, Some(test_buffer(&provider, &[2]))),
11480                ),
11481                (
11482                    "fact".to_string(),
11483                    RelationDelta::new(None, Some(test_buffer(&provider, &[3]))),
11484                ),
11485                (
11486                    "fact".to_string(),
11487                    RelationDelta::new(Some(test_buffer(&provider, &[3, 4])), None),
11488                ),
11489            ],
11490            &BTreeSet::from(["fact".to_string()]),
11491        )?;
11492
11493        assert_eq!(insert_rows(&provider, &prepared, "fact"), vec![1, 4]);
11494        let cancellations = prepared
11495            .cancellations()
11496            .get("fact")
11497            .expect("fact cancellation trace");
11498        assert_eq!(cancellations.len(), 2);
11499        assert_eq!(cancellations[0].update_index(), 1);
11500        assert_eq!(
11501            cancellations[0].incoming_direction(),
11502            RelationDeltaDirection::Delete
11503        );
11504        assert_eq!(sorted_u32(&provider, cancellations[0].tuples()), vec![2]);
11505        assert_eq!(cancellations[1].update_index(), 3);
11506        assert_eq!(
11507            cancellations[1].incoming_direction(),
11508            RelationDeltaDirection::Insert
11509        );
11510        assert_eq!(sorted_u32(&provider, cancellations[1].tuples()), vec![3]);
11511        Ok(())
11512    }
11513
11514    #[test]
11515    fn cancellation_trace_distinguishes_canceled_and_surviving_insert_occurrences() -> Result<()> {
11516        let Some(provider) = test_provider() else {
11517            return Ok(());
11518        };
11519        let program = LogicProgram::compile("pred fact(u32).")?;
11520
11521        let insert_delete_insert = program.prepare_relation_delta_batch(
11522            provider.as_ref(),
11523            vec![
11524                (
11525                    "fact".to_string(),
11526                    RelationDelta::new(Some(test_buffer(&provider, &[7])), None),
11527                ),
11528                (
11529                    "fact".to_string(),
11530                    RelationDelta::new(None, Some(test_buffer(&provider, &[7]))),
11531                ),
11532                (
11533                    "fact".to_string(),
11534                    RelationDelta::new(Some(test_buffer(&provider, &[7])), None),
11535                ),
11536            ],
11537            &BTreeSet::from(["fact".to_string()]),
11538        )?;
11539        assert_eq!(
11540            insert_rows(&provider, &insert_delete_insert, "fact"),
11541            vec![7]
11542        );
11543        let first_trace = insert_delete_insert
11544            .cancellations()
11545            .get("fact")
11546            .expect("first cancellation trace");
11547        assert_eq!(first_trace.len(), 1);
11548        assert_eq!(first_trace[0].update_index(), 1);
11549        assert_eq!(
11550            first_trace[0].incoming_direction(),
11551            RelationDeltaDirection::Delete
11552        );
11553        assert_eq!(sorted_u32(&provider, first_trace[0].tuples()), vec![7]);
11554
11555        let delete_insert_insert = program.prepare_relation_delta_batch(
11556            provider.as_ref(),
11557            vec![
11558                (
11559                    "fact".to_string(),
11560                    RelationDelta::new(None, Some(test_buffer(&provider, &[7]))),
11561                ),
11562                (
11563                    "fact".to_string(),
11564                    RelationDelta::new(Some(test_buffer(&provider, &[7])), None),
11565                ),
11566                (
11567                    "fact".to_string(),
11568                    RelationDelta::new(Some(test_buffer(&provider, &[7])), None),
11569                ),
11570            ],
11571            &BTreeSet::from(["fact".to_string()]),
11572        )?;
11573        assert_eq!(
11574            insert_rows(&provider, &delete_insert_insert, "fact"),
11575            vec![7]
11576        );
11577        let second_trace = delete_insert_insert
11578            .cancellations()
11579            .get("fact")
11580            .expect("second cancellation trace");
11581        assert_eq!(second_trace.len(), 1);
11582        assert_eq!(second_trace[0].update_index(), 1);
11583        assert_eq!(
11584            second_trace[0].incoming_direction(),
11585            RelationDeltaDirection::Insert
11586        );
11587        assert_eq!(sorted_u32(&provider, second_trace[0].tuples()), vec![7]);
11588        Ok(())
11589    }
11590
11591    #[test]
11592    fn raw_combined_delta_preserves_delete_then_insert_while_batch_cancels() -> Result<()> {
11593        let Some(provider) = test_provider() else {
11594            return Ok(());
11595        };
11596        let program = LogicProgram::compile("pred fact(u32).")?;
11597
11598        let mut raw_store = program.create_relation_store(provider.clone())?;
11599        raw_store.put("fact", test_buffer(&provider, &[7]));
11600        let mut raw_cache = None;
11601        let mut raw_runtime = None;
11602        let mut raw_delta = HashMap::new();
11603        raw_delta.insert(
11604            "fact".to_string(),
11605            RelationDelta::new(
11606                Some(test_buffer(&provider, &[7])),
11607                Some(test_buffer(&provider, &[7])),
11608            ),
11609        );
11610
11611        let raw_commit = program.prepare_relation_deltas_commit_with_session_runtime(
11612            provider.clone(),
11613            &mut raw_store,
11614            &mut raw_cache,
11615            &mut raw_runtime,
11616            raw_delta,
11617        )?;
11618        let raw_report = raw_commit.commit();
11619        assert_eq!(
11620            sorted_u32(&provider, raw_store.get("fact").unwrap()),
11621            vec![7]
11622        );
11623        assert_eq!(raw_report.insert_rows, 1);
11624        assert_eq!(raw_report.delete_rows, 1);
11625        assert_eq!(raw_report.canceled_rows, 0);
11626        assert_eq!(raw_report.changed_relations, 1);
11627
11628        let mut batch_store = program.create_relation_store(provider.clone())?;
11629        batch_store.put("fact", test_buffer(&provider, &[7]));
11630        let before_batch_version = batch_store.version("fact");
11631        let mut batch_cache = None;
11632        let mut batch_runtime = None;
11633        let batch = program.prepare_relation_delta_batch(
11634            provider.as_ref(),
11635            vec![
11636                (
11637                    "fact".to_string(),
11638                    RelationDelta::new(Some(test_buffer(&provider, &[7])), None),
11639                ),
11640                (
11641                    "fact".to_string(),
11642                    RelationDelta::new(None, Some(test_buffer(&provider, &[7]))),
11643                ),
11644            ],
11645            &BTreeSet::from(["fact".to_string()]),
11646        )?;
11647        let batch_commit = program.prepare_relation_delta_commit_with_session_runtime(
11648            provider.clone(),
11649            &mut batch_store,
11650            &mut batch_cache,
11651            &mut batch_runtime,
11652            batch,
11653        )?;
11654        let batch_report = batch_commit.commit();
11655        assert_eq!(batch_store.version("fact"), before_batch_version);
11656        assert_eq!(
11657            sorted_u32(&provider, batch_store.get("fact").unwrap()),
11658            vec![7]
11659        );
11660        assert_eq!(batch_report.insert_rows, 0);
11661        assert_eq!(batch_report.delete_rows, 0);
11662        assert_eq!(batch_report.canceled_rows, 1);
11663        assert_eq!(batch_report.changed_relations, 0);
11664        Ok(())
11665    }
11666
11667    #[test]
11668    fn cancellation_capture_is_scoped_to_selected_relations() -> Result<()> {
11669        let Some(provider) = test_provider() else {
11670            return Ok(());
11671        };
11672        let program = LogicProgram::compile(
11673            r#"
11674                pred evidenced(u32).
11675                pred positional(u32).
11676            "#,
11677        )?;
11678        let prepared = program.prepare_relation_delta_batch(
11679            provider.as_ref(),
11680            vec![
11681                (
11682                    "evidenced".to_string(),
11683                    RelationDelta::new(Some(test_buffer(&provider, &[1])), None),
11684                ),
11685                (
11686                    "positional".to_string(),
11687                    RelationDelta::new(Some(test_buffer(&provider, &[2])), None),
11688                ),
11689                (
11690                    "evidenced".to_string(),
11691                    RelationDelta::new(None, Some(test_buffer(&provider, &[1]))),
11692                ),
11693                (
11694                    "positional".to_string(),
11695                    RelationDelta::new(None, Some(test_buffer(&provider, &[2]))),
11696                ),
11697                (
11698                    "evidenced".to_string(),
11699                    RelationDelta::new(None, Some(test_buffer(&provider, &[3]))),
11700                ),
11701                (
11702                    "evidenced".to_string(),
11703                    RelationDelta::new(Some(test_buffer(&provider, &[3])), None),
11704                ),
11705            ],
11706            &BTreeSet::from(["evidenced".to_string()]),
11707        )?;
11708
11709        assert_eq!(
11710            prepared
11711                .cancellations()
11712                .keys()
11713                .map(String::as_str)
11714                .collect::<BTreeSet<_>>(),
11715            BTreeSet::from(["evidenced"])
11716        );
11717        let evidenced = prepared
11718            .cancellations()
11719            .get("evidenced")
11720            .expect("selected relation cancellation trace");
11721        assert_eq!(
11722            evidenced
11723                .iter()
11724                .map(RelationDeltaCancellation::update_index)
11725                .collect::<Vec<_>>(),
11726            vec![2, 5]
11727        );
11728        assert_eq!(prepared.report_seed.canceled_rows, 3);
11729        Ok(())
11730    }
11731
11732    #[test]
11733    fn disabled_cancellation_capture_preserves_net_data_and_stats_without_trace_work() -> Result<()>
11734    {
11735        let Some(provider) = test_provider() else {
11736            return Ok(());
11737        };
11738        let program = LogicProgram::compile(
11739            r#"
11740                pred fact(u32).
11741                pred out(u32).
11742                out(X) :- fact(X).
11743                ?- out(X).
11744            "#,
11745        )?;
11746        let mut uncaptured_store = program.create_relation_store(provider.clone())?;
11747        let mut uncaptured_cache = None;
11748        let mut uncaptured_runtime = None;
11749        let mut captured_store = program.create_relation_store(provider.clone())?;
11750        let mut captured_cache = None;
11751        let mut captured_runtime = None;
11752
11753        let uncaptured_batch = cancellation_batch(&provider);
11754        provider.memory().reset_alloc_count();
11755        let uncaptured = program.prepare_relation_delta_batch(
11756            provider.as_ref(),
11757            uncaptured_batch,
11758            &BTreeSet::new(),
11759        )?;
11760        let uncaptured_allocations = provider.memory().alloc_count();
11761        assert!(uncaptured.cancellations().is_empty());
11762        assert_eq!(insert_rows(&provider, &uncaptured, "fact"), vec![6]);
11763        let uncaptured_commit = program.prepare_relation_delta_commit_with_session_runtime(
11764            provider.clone(),
11765            &mut uncaptured_store,
11766            &mut uncaptured_cache,
11767            &mut uncaptured_runtime,
11768            uncaptured,
11769        )?;
11770        let uncaptured_report = uncaptured_commit.commit();
11771
11772        let captured_batch = cancellation_batch(&provider);
11773        provider.memory().reset_alloc_count();
11774        let captured = program.prepare_relation_delta_batch(
11775            provider.as_ref(),
11776            captured_batch,
11777            &BTreeSet::from(["fact".to_string()]),
11778        )?;
11779        let captured_allocations = provider.memory().alloc_count();
11780        assert_eq!(insert_rows(&provider, &captured, "fact"), vec![6]);
11781        assert_eq!(
11782            captured
11783                .cancellations()
11784                .get("fact")
11785                .expect("captured cancellation")
11786                .len(),
11787            1
11788        );
11789        assert!(
11790            captured_allocations > uncaptured_allocations,
11791            "capturing cancellation tuples should add device allocation requests in this fixture: captured={captured_allocations}, uncaptured={uncaptured_allocations}"
11792        );
11793        let captured_commit = program.prepare_relation_delta_commit_with_session_runtime(
11794            provider.clone(),
11795            &mut captured_store,
11796            &mut captured_cache,
11797            &mut captured_runtime,
11798            captured,
11799        )?;
11800        let captured_report = captured_commit.commit();
11801
11802        assert_eq!(
11803            report_counts(&uncaptured_report),
11804            report_counts(&captured_report)
11805        );
11806        assert_eq!(report_counts(&uncaptured_report), (3, 1, 1, 0, 1));
11807        assert_eq!(
11808            sorted_u32(
11809                &provider,
11810                uncaptured_store.get("fact").expect("uncaptured base fact")
11811            ),
11812            vec![6]
11813        );
11814        assert_eq!(
11815            sorted_u32(
11816                &provider,
11817                captured_store.get("fact").expect("captured base fact")
11818            ),
11819            vec![6]
11820        );
11821        Ok(())
11822    }
11823
11824    #[test]
11825    fn retained_preparation_failure_discards_mutated_runtime_without_base_puts() -> Result<()> {
11826        let Some(provider) = test_provider() else {
11827            return Ok(());
11828        };
11829        let program = LogicProgram::compile(
11830            r#"
11831                pred safe(u32).
11832                pred forbidden(u32).
11833                pred out(u32).
11834                out(X) :- safe(X).
11835                :- forbidden(X).
11836                ?- out(X).
11837            "#,
11838        )?;
11839        let mut base_store = program.create_relation_store(provider.clone())?;
11840        let safe_version = base_store.version("safe").expect("safe version");
11841        let forbidden_version = base_store.version("forbidden").expect("forbidden version");
11842        let mut runtime =
11843            Some(program.create_session_runtime(provider.clone(), &base_store, false)?);
11844        let (_, initial_cache) = program.evaluate_with_session_runtime(
11845            provider.clone(),
11846            runtime.as_mut().expect("initial runtime"),
11847        )?;
11848        let mut cache = Some(initial_cache);
11849        let prepared = program.prepare_relation_delta_batch(
11850            provider.as_ref(),
11851            vec![
11852                (
11853                    "safe".to_string(),
11854                    RelationDelta::new(Some(test_buffer(&provider, &[1])), None),
11855                ),
11856                (
11857                    "forbidden".to_string(),
11858                    RelationDelta::new(Some(test_buffer(&provider, &[9])), None),
11859                ),
11860            ],
11861            &BTreeSet::new(),
11862        )?;
11863
11864        let error = match program.prepare_relation_delta_commit_with_session_runtime(
11865            provider.clone(),
11866            &mut base_store,
11867            &mut cache,
11868            &mut runtime,
11869            prepared,
11870        ) {
11871            Ok(_) => panic!("constraint-violating preparation must fail"),
11872            Err(error) => error,
11873        };
11874
11875        assert!(
11876            error.to_string().contains("Constraint 0 violated"),
11877            "unexpected preparation error: {error}"
11878        );
11879        assert!(cache.is_none(), "failed preparation must discard cache");
11880        assert!(runtime.is_none(), "failed preparation must discard runtime");
11881        assert_eq!(base_store.version("safe"), Some(safe_version));
11882        assert_eq!(base_store.version("forbidden"), Some(forbidden_version));
11883        assert_eq!(base_store.get("safe").expect("safe base").num_rows(), 0);
11884        assert_eq!(
11885            base_store
11886                .get("forbidden")
11887                .expect("forbidden base")
11888                .num_rows(),
11889            0
11890        );
11891        Ok(())
11892    }
11893
11894    #[test]
11895    fn successful_preparation_binds_and_stages_all_state_until_infallible_commit() -> Result<()> {
11896        let Some(provider) = test_provider() else {
11897            return Ok(());
11898        };
11899        let program = LogicProgram::compile(
11900            r#"
11901                pred left_input(u32).
11902                pred right_input(u32).
11903                pred out(u32).
11904                out(X) :- left_input(X).
11905                out(X) :- right_input(X).
11906                ?- out(X).
11907            "#,
11908        )?;
11909        let mut base_store = program.create_relation_store(provider.clone())?;
11910        let left_version = base_store
11911            .version("left_input")
11912            .expect("left input version");
11913        let right_version = base_store
11914            .version("right_input")
11915            .expect("right input version");
11916        let mut runtime =
11917            Some(program.create_session_runtime(provider.clone(), &base_store, false)?);
11918        let (_, initial_cache) = program.evaluate_with_session_runtime(
11919            provider.clone(),
11920            runtime.as_mut().expect("initial runtime"),
11921        )?;
11922        let mut cache = Some(initial_cache);
11923        let authoritative_store_pointer = &mut base_store as *mut RelationStore;
11924        let cache_slot_pointer = &mut cache as *mut Option<LogicMaterializedStore>;
11925        let runtime_slot_pointer = &mut runtime as *mut Option<LogicSessionRuntime>;
11926        let prepared = program.prepare_relation_delta_batch(
11927            provider.as_ref(),
11928            vec![
11929                (
11930                    "left_input".to_string(),
11931                    RelationDelta::new(Some(test_buffer(&provider, &[1])), None),
11932                ),
11933                (
11934                    "right_input".to_string(),
11935                    RelationDelta::new(Some(test_buffer(&provider, &[2])), None),
11936                ),
11937            ],
11938            &BTreeSet::new(),
11939        )?;
11940
11941        let commit = program.prepare_relation_delta_commit_with_session_runtime(
11942            provider.clone(),
11943            &mut base_store,
11944            &mut cache,
11945            &mut runtime,
11946            prepared,
11947        )?;
11948
11949        assert!(
11950            std::ptr::eq(
11951                &*commit.authoritative_relation_store,
11952                authoritative_store_pointer
11953            ),
11954            "prepared commit must remain bound to its authoritative store"
11955        );
11956        assert!(
11957            std::ptr::eq(&*commit.cached_store_slot, cache_slot_pointer),
11958            "prepared commit must remain bound to its cache slot"
11959        );
11960        assert!(
11961            std::ptr::eq(&*commit.session_runtime_slot, runtime_slot_pointer),
11962            "prepared commit must remain bound to its runtime slot"
11963        );
11964        assert!(
11965            commit.cached_store_slot.is_none(),
11966            "prepared cache must be transaction-owned"
11967        );
11968        assert!(
11969            commit.session_runtime_slot.is_none(),
11970            "prepared runtime must be transaction-owned"
11971        );
11972        assert_eq!(
11973            commit.authoritative_relation_store.version("left_input"),
11974            Some(left_version)
11975        );
11976        assert_eq!(
11977            commit.authoritative_relation_store.version("right_input"),
11978            Some(right_version)
11979        );
11980        assert_eq!(
11981            commit
11982                .authoritative_relation_store
11983                .get("left_input")
11984                .expect("left input base")
11985                .num_rows(),
11986            0
11987        );
11988        assert_eq!(
11989            commit
11990                .authoritative_relation_store
11991                .get("right_input")
11992                .expect("right input base")
11993                .num_rows(),
11994            0
11995        );
11996        assert_eq!(commit.staged_base_updates.len(), 2);
11997        assert!(commit.prospective_cached_store.is_some());
11998        assert!(commit.prospective_session_runtime.is_some());
11999
12000        let prospective_store = commit.prospective_derived_store();
12001        assert_eq!(
12002            sorted_u32(
12003                &provider,
12004                prospective_store
12005                    .get("left_input")
12006                    .expect("prospective left input")
12007            ),
12008            vec![1]
12009        );
12010        assert_eq!(
12011            sorted_u32(
12012                &provider,
12013                prospective_store
12014                    .get("right_input")
12015                    .expect("prospective right input")
12016            ),
12017            vec![2]
12018        );
12019
12020        provider.memory().reset_alloc_count();
12021        let report = commit.commit();
12022        assert_eq!(
12023            provider.memory().alloc_count(),
12024            0,
12025            "commit must issue zero GPU allocation requests because preparation already staged every buffer"
12026        );
12027
12028        assert_eq!(base_store.version("left_input"), Some(left_version + 1));
12029        assert_eq!(base_store.version("right_input"), Some(right_version + 1));
12030        assert_eq!(
12031            sorted_u32(
12032                &provider,
12033                base_store.get("left_input").expect("committed left input")
12034            ),
12035            vec![1]
12036        );
12037        assert_eq!(
12038            sorted_u32(
12039                &provider,
12040                base_store
12041                    .get("right_input")
12042                    .expect("committed right input")
12043            ),
12044            vec![2]
12045        );
12046        let result = program.evaluate_cached_relation_store(
12047            provider.clone(),
12048            cache.as_ref().expect("committed cache"),
12049        )?;
12050        assert_eq!(sorted_u32(&provider, &result.queries[0].buffer), vec![1, 2]);
12051        assert!(runtime.is_some(), "commit must install the runtime");
12052        assert_eq!(report_counts(&report), (2, 2, 2, 0, 0));
12053        Ok(())
12054    }
12055
12056    #[test]
12057    fn prospective_base_snapshot_recomputes_deletion_without_stale_derived_rows() -> Result<()> {
12058        let Some(provider) = test_provider() else {
12059            return Ok(());
12060        };
12061        let program = LogicProgram::compile(
12062            r#"
12063                pred fact(u32).
12064                pred out(u32).
12065                out(X) :- fact(X).
12066                ?- out(X).
12067            "#,
12068        )?;
12069        let mut base_store = program.create_relation_store(provider.clone())?;
12070        base_store.put("fact", test_buffer(&provider, &[1, 2]));
12071        let mut runtime =
12072            Some(program.create_session_runtime(provider.clone(), &base_store, false)?);
12073        let (_, initial_cache) = program.evaluate_with_session_runtime(
12074            provider.clone(),
12075            runtime.as_mut().expect("initial runtime"),
12076        )?;
12077        let mut cache = Some(initial_cache);
12078        drop(base_store.remove("fact").expect("authoritative fact"));
12079
12080        let prepared = program.prepare_relation_delta_batch(
12081            provider.as_ref(),
12082            vec![(
12083                "fact".to_string(),
12084                RelationDelta::new(None, Some(test_buffer(&provider, &[2]))),
12085            )],
12086            &BTreeSet::new(),
12087        )?;
12088        let commit = program.prepare_relation_delta_commit_with_session_runtime(
12089            provider.clone(),
12090            &mut base_store,
12091            &mut cache,
12092            &mut runtime,
12093            prepared,
12094        )?;
12095
12096        let prospective_derived = commit.prospective_derived_store();
12097        assert_eq!(
12098            sorted_u32(
12099                &provider,
12100                prospective_derived
12101                    .get("__xlog_query_0")
12102                    .expect("prospective query")
12103            ),
12104            vec![1]
12105        );
12106
12107        provider.memory().reset_alloc_count();
12108        let prospective_base = commit.clone_prospective_base_store()?;
12109        assert_eq!(
12110            provider.memory().alloc_count(),
12111            4,
12112            "one-column authoritative and staged relations should each be cloned exactly once"
12113        );
12114        assert_eq!(
12115            sorted_u32(
12116                &provider,
12117                prospective_base
12118                    .get("fact")
12119                    .expect("staged missing base relation")
12120            ),
12121            vec![1]
12122        );
12123        assert_eq!(
12124            prospective_base
12125                .get("out")
12126                .expect("empty authoritative derived relation")
12127                .num_rows(),
12128            0
12129        );
12130        let (_, independently_recomputed) = program.evaluate_with_relation_store_and_cache(
12131            provider.clone(),
12132            &prospective_base,
12133            false,
12134        )?;
12135        assert!(program.relation_stores_query_equivalent(
12136            provider.as_ref(),
12137            independently_recomputed.as_relation_store(),
12138            prospective_derived,
12139        )?);
12140
12141        let mut stale_derived_seed = commit.clone_prospective_base_store()?;
12142        stale_derived_seed.put("out", test_buffer(&provider, &[1, 2]));
12143        let (_, stale_seed_recompute) = program.evaluate_with_relation_store_and_cache(
12144            provider.clone(),
12145            &stale_derived_seed,
12146            false,
12147        )?;
12148        assert_eq!(
12149            sorted_u32(
12150                &provider,
12151                stale_seed_recompute
12152                    .as_relation_store()
12153                    .get("__xlog_query_0")
12154                    .expect("stale-seeded query")
12155            ),
12156            vec![1, 2],
12157            "seeding full recompute with an intensional head retains the deleted row"
12158        );
12159        Ok(())
12160    }
12161
12162    #[test]
12163    fn prospective_base_snapshot_skips_superseded_authoritative_buffer_clone() -> Result<()> {
12164        let Some(provider) = test_provider() else {
12165            return Ok(());
12166        };
12167        let program = LogicProgram::compile(
12168            r#"
12169                pred fact(u32).
12170                pred out(u32).
12171                out(X) :- fact(X).
12172                ?- out(X).
12173            "#,
12174        )?;
12175        let mut base_store = program.create_relation_store(provider.clone())?;
12176        base_store.put("fact", test_buffer(&provider, &[1, 2]));
12177        let mut runtime =
12178            Some(program.create_session_runtime(provider.clone(), &base_store, false)?);
12179        let (_, initial_cache) = program.evaluate_with_session_runtime(
12180            provider.clone(),
12181            runtime.as_mut().expect("initial runtime"),
12182        )?;
12183        let mut cache = Some(initial_cache);
12184        let prepared = program.prepare_relation_delta_batch(
12185            provider.as_ref(),
12186            vec![(
12187                "fact".to_string(),
12188                RelationDelta::new(None, Some(test_buffer(&provider, &[2]))),
12189            )],
12190            &BTreeSet::new(),
12191        )?;
12192        let commit = program.prepare_relation_delta_commit_with_session_runtime(
12193            provider.clone(),
12194            &mut base_store,
12195            &mut cache,
12196            &mut runtime,
12197            prepared,
12198        )?;
12199
12200        provider.memory().reset_alloc_count();
12201        let prospective_base = commit.clone_prospective_base_store()?;
12202        assert_eq!(
12203            provider.memory().alloc_count(),
12204            4,
12205            "the empty authoritative head and final staged base must each be cloned once"
12206        );
12207        assert_eq!(
12208            sorted_u32(
12209                &provider,
12210                prospective_base.get("fact").expect("prospective fact")
12211            ),
12212            vec![1]
12213        );
12214        Ok(())
12215    }
12216
12217    #[test]
12218    fn prospective_base_clone_budget_failure_discards_prepared_transaction() -> Result<()> {
12219        let Some(calibration_provider) = test_provider() else {
12220            return Ok(());
12221        };
12222        let program = LogicProgram::compile(
12223            r#"
12224                pred alpha_input(u32).
12225                pred stable_input(u32).
12226            "#,
12227        )?;
12228        let stable_rows = (10_000..75_536).collect::<Vec<u32>>();
12229
12230        let mut calibration_store = program.create_relation_store(calibration_provider.clone())?;
12231        calibration_store.put(
12232            "stable_input",
12233            test_buffer(&calibration_provider, &stable_rows),
12234        );
12235        let mut calibration_runtime = Some(program.create_session_runtime(
12236            calibration_provider.clone(),
12237            &calibration_store,
12238            false,
12239        )?);
12240        let (_, calibration_cache) = program.evaluate_with_session_runtime(
12241            calibration_provider.clone(),
12242            calibration_runtime.as_mut().expect("calibration runtime"),
12243        )?;
12244        let mut calibration_cache = Some(calibration_cache);
12245        let calibration_batch = program.prepare_relation_delta_batch(
12246            calibration_provider.as_ref(),
12247            vec![(
12248                "alpha_input".to_string(),
12249                RelationDelta::new(Some(test_buffer(&calibration_provider, &[1])), None),
12250            )],
12251            &BTreeSet::new(),
12252        )?;
12253        calibration_provider.memory().reset_peak();
12254        let calibration_commit = program.prepare_relation_delta_commit_with_session_runtime(
12255            calibration_provider.clone(),
12256            &mut calibration_store,
12257            &mut calibration_cache,
12258            &mut calibration_runtime,
12259            calibration_batch,
12260        )?;
12261        assert_eq!(calibration_commit.staged_base_updates.len(), 1);
12262        assert_eq!(calibration_commit.staged_base_updates[0].0, "alpha_input");
12263        assert_eq!(calibration_commit.staged_base_updates[0].1.num_rows(), 1);
12264        let preparation_peak = calibration_provider.memory().peak_bytes();
12265        drop(calibration_commit);
12266        drop(calibration_store);
12267        drop(calibration_cache);
12268        drop(calibration_runtime);
12269        drop(calibration_provider);
12270
12271        let tight_budget = preparation_peak
12272            .checked_add(4096)
12273            .expect("calibrated preparation budget must fit in u64");
12274        let tight_provider = test_provider_with_budget(tight_budget)
12275            .expect("calibrated byte budget must construct a CUDA provider");
12276        let mut base_store = program.create_relation_store(tight_provider.clone())?;
12277        base_store.put("stable_input", test_buffer(&tight_provider, &stable_rows));
12278        let authoritative_gpu_bytes = tight_provider.memory().allocated_bytes();
12279        let alpha_version = base_store.version("alpha_input").expect("alpha version");
12280        let stable_version = base_store.version("stable_input").expect("stable version");
12281        let mut runtime =
12282            Some(program.create_session_runtime(tight_provider.clone(), &base_store, false)?);
12283        let (_, initial_cache) = program.evaluate_with_session_runtime(
12284            tight_provider.clone(),
12285            runtime.as_mut().expect("initial runtime"),
12286        )?;
12287        let mut cache = Some(initial_cache);
12288        let prepared_batch = program.prepare_relation_delta_batch(
12289            tight_provider.as_ref(),
12290            vec![(
12291                "alpha_input".to_string(),
12292                RelationDelta::new(Some(test_buffer(&tight_provider, &[1])), None),
12293            )],
12294            &BTreeSet::new(),
12295        )?;
12296        tight_provider.memory().reset_peak();
12297        let prepared_commit = program.prepare_relation_delta_commit_with_session_runtime(
12298            tight_provider.clone(),
12299            &mut base_store,
12300            &mut cache,
12301            &mut runtime,
12302            prepared_batch,
12303        )?;
12304        assert_eq!(prepared_commit.staged_base_updates.len(), 1);
12305        assert_eq!(prepared_commit.staged_base_updates[0].0, "alpha_input");
12306        assert_eq!(prepared_commit.staged_base_updates[0].1.num_rows(), 1);
12307        assert!(
12308            tight_provider.memory().peak_bytes() <= preparation_peak,
12309            "identical preparation must fit within the calibrated peak"
12310        );
12311
12312        let stable_clone_bytes = u64::try_from(stable_rows.len())
12313            .expect("stable row count must fit in u64")
12314            .checked_mul(u64::try_from(std::mem::size_of::<u32>()).expect("u32 width fits in u64"))
12315            .and_then(|bytes| {
12316                bytes.checked_add(
12317                    u64::try_from(std::mem::size_of::<u32>())
12318                        .expect("device row-count width fits in u64"),
12319                )
12320            })
12321            .expect("stable clone size must fit in u64");
12322        let staged_column_bytes =
12323            u64::try_from(std::mem::size_of::<u32>()).expect("staged column width fits in u64");
12324        let clone_headroom = stable_clone_bytes
12325            .checked_add(staged_column_bytes - 1)
12326            .expect("clone headroom must fit in u64");
12327        let pressure_bytes = tight_provider
12328            .memory()
12329            .remaining_bytes()
12330            .checked_sub(clone_headroom)
12331            .expect("calibrated provider must have room for the authoritative clone");
12332        let pressure_len = usize::try_from(pressure_bytes)
12333            .expect("calibrated pressure allocation must fit in usize");
12334        let pressure_guard = tight_provider.memory().alloc::<u8>(pressure_len)?;
12335        let clone_start_bytes = tight_provider.memory().allocated_bytes();
12336        let expected_current_bytes = clone_start_bytes
12337            .checked_add(stable_clone_bytes)
12338            .expect("current bytes before the refused staged clone must fit in u64");
12339        let expected_required_bytes = expected_current_bytes
12340            .checked_add(staged_column_bytes)
12341            .expect("cumulative required bytes must fit in u64");
12342        assert_eq!(
12343            expected_required_bytes,
12344            tight_budget + 1,
12345            "the calibrated request must exceed the configured budget by exactly one byte"
12346        );
12347
12348        let error = match prepared_commit.clone_prospective_base_store() {
12349            Ok(_) => panic!("one-byte-tight prospective base cloning must fail"),
12350            Err(error) => error,
12351        };
12352        let XlogError::ResourceExhausted {
12353            context,
12354            estimated_bytes,
12355            budget_bytes,
12356        } = &error
12357        else {
12358            panic!("expected GPU resource exhaustion, got {error}");
12359        };
12360        assert_eq!(*budget_bytes, tight_budget);
12361        assert_eq!(*estimated_bytes, expected_required_bytes);
12362        assert_eq!(
12363            context,
12364            &format!(
12365                "cloning staged prospective base relation 'alpha_input': GPU memory pressure: layer=manager_alloc current_bytes={expected_current_bytes} requested_bytes={staged_column_bytes} required_bytes={expected_required_bytes} required_u64_overflow=false budget_bytes={tight_budget} prior_peak_bytes={expected_current_bytes}"
12366            ),
12367            "the authoritative base clone must complete before the staged overlay exhausts memory"
12368        );
12369        assert_eq!(
12370            tight_provider.memory().peak_bytes(),
12371            expected_current_bytes,
12372            "the refused request must not enter the admitted allocation high-water mark"
12373        );
12374        drop(prepared_commit);
12375
12376        assert!(cache.is_none(), "failed diagnostic must discard its cache");
12377        assert!(
12378            runtime.is_none(),
12379            "failed diagnostic must discard its retained runtime"
12380        );
12381        assert_eq!(
12382            tight_provider.memory().allocated_bytes(),
12383            authoritative_gpu_bytes + pressure_bytes,
12384            "dropping the prepared transaction must release every prospective buffer while preserving external memory pressure"
12385        );
12386        drop(pressure_guard);
12387        assert_eq!(
12388            tight_provider.memory().allocated_bytes(),
12389            authoritative_gpu_bytes,
12390            "releasing the pressure allocation must leave only authoritative data"
12391        );
12392        assert_eq!(base_store.version("alpha_input"), Some(alpha_version));
12393        assert_eq!(base_store.version("stable_input"), Some(stable_version));
12394        assert_eq!(
12395            base_store
12396                .get("alpha_input")
12397                .expect("authoritative alpha")
12398                .num_rows(),
12399            0
12400        );
12401        assert_eq!(
12402            sorted_u32(
12403                &tight_provider,
12404                base_store
12405                    .get("stable_input")
12406                    .expect("authoritative stable input")
12407            ),
12408            stable_rows
12409        );
12410        Ok(())
12411    }
12412
12413    #[test]
12414    fn later_snapshot_clone_failure_discards_staged_updates_and_derived_state() -> Result<()> {
12415        let Some(calibration_provider) = test_provider() else {
12416            return Ok(());
12417        };
12418        let program = LogicProgram::compile(
12419            r#"
12420                pred alpha_input(u32).
12421                pred omega_input(u32).
12422                pred stable_input(u32).
12423                pred out(u32).
12424                out(X) :- alpha_input(X).
12425                out(X) :- omega_input(X).
12426                ?- out(X).
12427            "#,
12428        )?;
12429        let alpha_rows = (0..512).collect::<Vec<u32>>();
12430        let omega_rows = (10_000..10_512).collect::<Vec<u32>>();
12431        let stable_rows = (20_000..85_536).collect::<Vec<u32>>();
12432
12433        let mut calibration_store = program.create_relation_store(calibration_provider.clone())?;
12434        calibration_store.put(
12435            "stable_input",
12436            test_buffer(&calibration_provider, &stable_rows),
12437        );
12438        let mut calibration_runtime = Some(program.create_session_runtime(
12439            calibration_provider.clone(),
12440            &calibration_store,
12441            false,
12442        )?);
12443        let (_, calibration_cache) = program.evaluate_with_session_runtime(
12444            calibration_provider.clone(),
12445            calibration_runtime.as_mut().expect("calibration runtime"),
12446        )?;
12447        let mut calibration_cache = Some(calibration_cache);
12448        let calibration_batch = program.prepare_relation_delta_batch(
12449            calibration_provider.as_ref(),
12450            vec![
12451                (
12452                    "alpha_input".to_string(),
12453                    RelationDelta::new(Some(test_buffer(&calibration_provider, &alpha_rows)), None),
12454                ),
12455                (
12456                    "omega_input".to_string(),
12457                    RelationDelta::new(Some(test_buffer(&calibration_provider, &omega_rows)), None),
12458                ),
12459            ],
12460            &BTreeSet::new(),
12461        )?;
12462        calibration_provider.memory().reset_peak();
12463        calibration_provider.memory().reset_alloc_count();
12464        let calibration_commit = program.prepare_relation_delta_commit_with_session_runtime(
12465            calibration_provider.clone(),
12466            &mut calibration_store,
12467            &mut calibration_cache,
12468            &mut calibration_runtime,
12469            calibration_batch,
12470        )?;
12471        let successful_peak = calibration_provider.memory().peak_bytes();
12472        let successful_preparation_allocations = calibration_provider.memory().alloc_count();
12473        assert!(
12474            successful_peak >= calibration_provider.memory().allocated_bytes(),
12475            "the calibrated peak must cover every live staged GPU allocation"
12476        );
12477        drop(calibration_commit);
12478        drop(calibration_store);
12479        drop(calibration_cache);
12480        drop(calibration_runtime);
12481        drop(calibration_provider);
12482
12483        let tight_budget = successful_peak
12484            .checked_sub(1)
12485            .expect("successful preparation must allocate GPU memory");
12486        let tight_provider = test_provider_with_budget(tight_budget)
12487            .expect("calibrated budget must still construct a CUDA provider");
12488        let mut base_store = program.create_relation_store(tight_provider.clone())?;
12489        base_store.put("stable_input", test_buffer(&tight_provider, &stable_rows));
12490        let authoritative_gpu_bytes = tight_provider.memory().allocated_bytes();
12491        let alpha_version = base_store.version("alpha_input").expect("alpha version");
12492        let omega_version = base_store.version("omega_input").expect("omega version");
12493        let stable_version = base_store.version("stable_input").expect("stable version");
12494        let mut runtime =
12495            Some(program.create_session_runtime(tight_provider.clone(), &base_store, false)?);
12496        let (_, initial_cache) = program.evaluate_with_session_runtime(
12497            tight_provider.clone(),
12498            runtime.as_mut().expect("initial runtime"),
12499        )?;
12500        let mut cache = Some(initial_cache);
12501        let prepared = program.prepare_relation_delta_batch(
12502            tight_provider.as_ref(),
12503            vec![
12504                (
12505                    "alpha_input".to_string(),
12506                    RelationDelta::new(Some(test_buffer(&tight_provider, &alpha_rows)), None),
12507                ),
12508                (
12509                    "omega_input".to_string(),
12510                    RelationDelta::new(Some(test_buffer(&tight_provider, &omega_rows)), None),
12511                ),
12512            ],
12513            &BTreeSet::new(),
12514        )?;
12515
12516        tight_provider.memory().reset_alloc_count();
12517        let final_row_count_clone_bytes =
12518            u64::try_from(std::mem::size_of::<u32>()).expect("device row-count width fits in u64");
12519        let expected_current_bytes = tight_budget
12520            .checked_sub(final_row_count_clone_bytes - 1)
12521            .expect("one-byte-tight budget must cover earlier snapshot clones");
12522        let expected_required_bytes = expected_current_bytes
12523            .checked_add(final_row_count_clone_bytes)
12524            .expect("cumulative required bytes must fit in u64");
12525        assert_eq!(
12526            expected_required_bytes,
12527            tight_budget + 1,
12528            "the calibrated final clone must exceed the configured budget by exactly one byte"
12529        );
12530        let error = match program.prepare_relation_delta_commit_with_session_runtime(
12531            tight_provider.clone(),
12532            &mut base_store,
12533            &mut cache,
12534            &mut runtime,
12535            prepared,
12536        ) {
12537            Ok(_) => panic!("one-byte-tight preparation must fail during the final clone"),
12538            Err(error) => error,
12539        };
12540        let XlogError::ResourceExhausted {
12541            context,
12542            estimated_bytes,
12543            budget_bytes,
12544        } = &error
12545        else {
12546            panic!("expected GPU resource exhaustion, got {error}");
12547        };
12548        assert_eq!(*budget_bytes, tight_budget);
12549        assert_eq!(*estimated_bytes, expected_required_bytes);
12550        assert_eq!(
12551            context,
12552            &format!(
12553                "cloning prospective relation snapshot 'stable_input': GPU memory pressure: layer=manager_alloc current_bytes={expected_current_bytes} requested_bytes={final_row_count_clone_bytes} required_bytes={expected_required_bytes} required_u64_overflow=false budget_bytes={tight_budget} prior_peak_bytes={expected_current_bytes}"
12554            )
12555        );
12556        assert_eq!(
12557            tight_provider.memory().peak_bytes(),
12558            expected_current_bytes,
12559            "the refused final clone must leave the high-water mark at the last admitted allocation"
12560        );
12561        let failed_preparation_allocations = tight_provider.memory().alloc_count();
12562        assert!(
12563            successful_preparation_allocations > 4,
12564            "calibration must include both staged-base and snapshot clones"
12565        );
12566        assert_eq!(
12567            failed_preparation_allocations,
12568            successful_preparation_allocations,
12569            "the one-byte-tight run must reach the final calibrated clone allocation after every earlier staged clone succeeds"
12570        );
12571        assert!(cache.is_none(), "failed preparation must discard its cache");
12572        assert!(
12573            runtime.is_none(),
12574            "failed preparation must discard its runtime"
12575        );
12576        assert_eq!(
12577            tight_provider.memory().allocated_bytes(),
12578            authoritative_gpu_bytes,
12579            "failed preparation must release every transaction-owned GPU allocation"
12580        );
12581        assert_eq!(base_store.version("alpha_input"), Some(alpha_version));
12582        assert_eq!(base_store.version("omega_input"), Some(omega_version));
12583        assert_eq!(base_store.version("stable_input"), Some(stable_version));
12584        assert_eq!(
12585            base_store
12586                .get("alpha_input")
12587                .expect("authoritative alpha")
12588                .num_rows(),
12589            0
12590        );
12591        assert_eq!(
12592            base_store
12593                .get("omega_input")
12594                .expect("authoritative omega")
12595                .num_rows(),
12596            0
12597        );
12598        assert_eq!(
12599            sorted_u32(
12600                &tight_provider,
12601                base_store
12602                    .get("stable_input")
12603                    .expect("authoritative stable input")
12604            ),
12605            stable_rows
12606        );
12607        Ok(())
12608    }
12609}