Skip to main content

xlog_gpu/
logic.rs

1//! GPU-accelerated evaluation of compiled Datalog programs.
2
3use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::{Arc, OnceLock};
6
7use xlog_core::{RelId, Result, ScalarType, Schema, XlogError};
8use xlog_cuda::device_runtime::{
9    AsyncCudaResource, DeviceMemoryResource, GlobalDeviceBudget, StreamPool, XlogDeviceRuntime,
10};
11use xlog_cuda::{CudaBuffer, CudaColumn, CudaKernelProvider};
12use xlog_ir::{EpistemicExecutablePlan, ExecutionPlan};
13use xlog_logic::ast::{PredColumn, PredDecl, TypeRef};
14use xlog_logic::epistemic::{
15    compile_epistemic_gpu_execution, compile_epistemic_gpu_split_execution,
16    epistemic_extensional_multi_arity_predicates, prepare_epistemic_program,
17    reduce_epistemic_program_to_ordinary,
18    reduce_epistemic_program_to_ordinary_for_stratified_schema,
19    try_plan_stratified_epistemic_program, try_prepare_g91_compatibility_reduction,
20    try_reduce_case_a_recursive_epistemic_program, try_reduce_prepared_recursive_epistemic_program,
21    EpistemicSplitExecutablePlan, G91CompatibilityReduction,
22};
23use xlog_logic::ground_term_encoding::append_ground_term_bytes;
24use xlog_logic::{
25    format_constraint_body, Atom, BodyLiteral, Compiler, Constraint, EpistemicLiteral, EpistemicOp,
26    Program, Query, Rule, Term,
27};
28use xlog_runtime::executor::JoinIndexCacheStats;
29use xlog_runtime::resident_graph::{
30    ResidentGraphCertifiedPlan, ResidentGraphCoreTransferStats, ResidentGraphDeclineReason,
31    ResidentGraphDeferredProfile, ResidentGraphExecutionError, ResidentGraphExecutionStats,
32    ResidentGraphFinalObservationStats, ResidentGraphPrepareOptions, ResidentGraphSchemaCatalog,
33    ResidentGraphSelectionKind,
34};
35use xlog_runtime::{
36    DeltaRecomputeStats, EpistemicGpuExecutionResult, EpistemicGpuWorkspaceCapacities,
37    ExecutionStats, Executor, OpStats, RelationDelta, RelationStore, StratumStats,
38};
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41enum ResidentSelectionMode {
42    Disabled,
43    Prefer,
44    Require,
45}
46
47impl ResidentSelectionMode {
48    fn from_env() -> Result<Self> {
49        let enabled = |name: &str| {
50            std::env::var(name)
51                .map(|value| !value.is_empty() && value != "0")
52                .unwrap_or(false)
53        };
54        let disabled = enabled("XLOG_DISABLE_RESIDENT_RECURSION");
55        let required = enabled("XLOG_REQUIRE_RESIDENT_RECURSION");
56        let preferred = enabled("XLOG_USE_RESIDENT_RECURSION");
57        if u8::from(disabled) + u8::from(required) + u8::from(preferred) > 1 {
58            return Err(XlogError::Execution(
59                "resident execution environment flags are mutually exclusive".to_string(),
60            ));
61        }
62        Ok(if disabled {
63            Self::Disabled
64        } else if required {
65            Self::Require
66        } else if preferred {
67            Self::Prefer
68        } else {
69            Self::Disabled
70        })
71    }
72
73    fn requested(self) -> bool {
74        self != Self::Disabled
75    }
76}
77
78struct ResidentCompletedProfile {
79    telemetry: ResidentGraphExecutionStats,
80    iterations: u32,
81}
82
83const RESIDENT_LATENCY_DIAGNOSTICS_ENV: &str = "XLOG_RESIDENT_LATENCY_DIAGNOSTICS";
84static RESIDENT_LATENCY_SAMPLE: AtomicU64 = AtomicU64::new(0);
85
86#[derive(Default)]
87struct ResidentLatencyDiagnostic {
88    sample: u64,
89    certificate_input_ns: u64,
90    certificate_cache_was_warm: bool,
91    certificate_initialized_here: bool,
92    certificate_initialization_ns: u64,
93    certificate_cache_access_ns: u64,
94    input_setup_ns: u64,
95    prepare_capture_allocation_ns: u64,
96    launch_submission_ns: u64,
97    sync_wall_ns: u64,
98    device_event_ns: u64,
99    receipt_d2h_ns: u64,
100    receipt_decode_schema_staging_ns: u64,
101    owner_teardown_residual_ns: u64,
102    commit_ns: u64,
103    result_stats_construction_ns: u64,
104    executor_store_teardown_ns: u64,
105    staged_outputs: u64,
106    relation_registrations: usize,
107    remaining_store_relations_before_drop: usize,
108    runtime_bytes: [usize; 8],
109    manager_bytes: [u64; 8],
110}
111
112#[cfg(test)]
113mod resident_latency_diagnostic_tests {
114    use super::{
115        finalized_resident_latency_diagnostic_lines, resident_latency_diagnostic_line,
116        ResidentLatencyDiagnostic,
117    };
118
119    #[test]
120    fn certificate_latency_distinguishes_cold_initialization_from_warm_access() {
121        let mut cold = ResidentLatencyDiagnostic::new();
122        cold.sample = 3;
123        cold.certificate_cache_was_warm = false;
124        cold.certificate_initialized_here = true;
125        cold.certificate_initialization_ns = 41;
126        let cold_line =
127            resident_latency_diagnostic_line(Some(&cold), 101).expect("cold diagnostic line");
128        assert!(cold_line.contains("sample=3"));
129        assert!(cold_line.contains("certificate_cache_was_warm=false"));
130        assert!(cold_line.contains("certificate_initialized_here=true"));
131        assert!(cold_line.contains("certificate_initialization_ns=41"));
132        assert!(cold_line.contains("certificate_cache_access_ns=0"));
133
134        let mut warm = ResidentLatencyDiagnostic::new();
135        warm.sample = 4;
136        warm.certificate_cache_was_warm = true;
137        warm.certificate_cache_access_ns = 7;
138        let warm_line =
139            resident_latency_diagnostic_line(Some(&warm), 102).expect("warm diagnostic line");
140        assert!(warm_line.contains("sample=4"));
141        assert!(warm_line.contains("certificate_cache_was_warm=true"));
142        assert!(warm_line.contains("certificate_initialized_here=false"));
143        assert!(warm_line.contains("certificate_initialization_ns=0"));
144        assert!(warm_line.contains("certificate_cache_access_ns=7"));
145        assert_eq!(resident_latency_diagnostic_line(None, 0), None);
146    }
147
148    #[test]
149    fn finalized_latency_diagnostics_derive_after_total_and_preserve_sample_order() {
150        let mut outer = ResidentLatencyDiagnostic::new();
151        outer.sample = 17;
152        let prepare_derived = std::cell::Cell::new(false);
153        let outer_derived = std::cell::Cell::new(false);
154
155        let lines = finalized_resident_latency_diagnostic_lines(
156            101,
157            Some(&outer),
158            Some(|| {
159                prepare_derived.set(true);
160                "resident prepare phases: sample=17 total_ns=53".to_string()
161            }),
162            |diagnostic, total_ns| {
163                outer_derived.set(true);
164                diagnostic.format_line(total_ns)
165            },
166        )
167        .expect("enabled diagnostics finalize one fixed pair");
168
169        assert!(prepare_derived.get());
170        assert!(outer_derived.get());
171        assert_eq!(lines.len(), 2);
172        assert!(lines[0]
173            .as_deref()
174            .is_some_and(|line| line.starts_with("resident prepare phases: sample=17 ")));
175        assert!(lines[1].as_deref().is_some_and(|line| {
176            line.starts_with("resident latency phases: sample=17 total_ns=101 ")
177        }));
178
179        let disabled_prepare_called = std::cell::Cell::new(false);
180        let disabled_outer_called = std::cell::Cell::new(false);
181        let disabled = finalized_resident_latency_diagnostic_lines(
182            0,
183            None,
184            Some(|| {
185                disabled_prepare_called.set(true);
186                String::new()
187            }),
188            |diagnostic, total_ns| {
189                disabled_outer_called.set(true);
190                diagnostic.format_line(total_ns)
191            },
192        );
193        assert!(disabled.is_none());
194        assert!(!disabled_prepare_called.get());
195        assert!(!disabled_outer_called.get());
196    }
197}
198
199impl ResidentLatencyDiagnostic {
200    fn new() -> Self {
201        Self {
202            sample: RESIDENT_LATENCY_SAMPLE.fetch_add(1, Ordering::Relaxed),
203            ..Self::default()
204        }
205    }
206
207    fn format_line(&self, total_ns: u64) -> String {
208        let additive_phases = [
209            self.certificate_input_ns,
210            self.prepare_capture_allocation_ns,
211            self.launch_submission_ns,
212            self.sync_wall_ns,
213            self.receipt_d2h_ns,
214            self.receipt_decode_schema_staging_ns,
215            self.owner_teardown_residual_ns,
216            self.commit_ns,
217            self.result_stats_construction_ns,
218            self.executor_store_teardown_ns,
219        ];
220        let unattributed_host_ns = resident_latency_unattributed_ns(total_ns, &additive_phases);
221        let owner_runtime_bytes_released =
222            self.runtime_bytes[4].saturating_sub(self.runtime_bytes[5]);
223        let owner_manager_bytes_released =
224            self.manager_bytes[4].saturating_sub(self.manager_bytes[5]);
225        let executor_runtime_bytes_released =
226            self.runtime_bytes[6].saturating_sub(self.runtime_bytes[7]);
227        let executor_manager_bytes_released =
228            self.manager_bytes[6].saturating_sub(self.manager_bytes[7]);
229        format!(
230            "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",
231            self.sample,
232            total_ns,
233            self.certificate_input_ns,
234            self.certificate_cache_was_warm,
235            self.certificate_initialized_here,
236            self.certificate_initialization_ns,
237            self.certificate_cache_access_ns,
238            self.input_setup_ns,
239            self.certificate_input_ns
240                .saturating_sub(self.certificate_initialization_ns)
241                .saturating_sub(self.certificate_cache_access_ns)
242                .saturating_sub(self.input_setup_ns),
243            self.prepare_capture_allocation_ns,
244            self.launch_submission_ns,
245            self.sync_wall_ns,
246            self.device_event_ns,
247            self.receipt_d2h_ns,
248            self.receipt_decode_schema_staging_ns,
249            self.owner_teardown_residual_ns,
250            self.commit_ns,
251            self.result_stats_construction_ns,
252            self.executor_store_teardown_ns,
253            unattributed_host_ns,
254            self.staged_outputs,
255            self.relation_registrations,
256            self.remaining_store_relations_before_drop,
257            self.runtime_bytes,
258            self.manager_bytes,
259            owner_runtime_bytes_released,
260            owner_manager_bytes_released,
261            executor_runtime_bytes_released,
262            executor_manager_bytes_released,
263        )
264    }
265}
266
267fn resident_latency_diagnostic_line(
268    diagnostic: Option<&ResidentLatencyDiagnostic>,
269    total_ns: u64,
270) -> Option<String> {
271    diagnostic.map(|diagnostic| diagnostic.format_line(total_ns))
272}
273
274fn finalized_resident_latency_diagnostic_lines<F, G>(
275    total_ns: u64,
276    diagnostic: Option<&ResidentLatencyDiagnostic>,
277    prepare_line: Option<F>,
278    format_outer: G,
279) -> Option<[Option<String>; 2]>
280where
281    F: FnOnce() -> String,
282    G: FnOnce(&ResidentLatencyDiagnostic, u64) -> String,
283{
284    let diagnostic = diagnostic?;
285    Some([
286        prepare_line.map(|prepare_line| prepare_line()),
287        Some(format_outer(diagnostic, total_ns)),
288    ])
289}
290
291fn resident_latency_diagnostics_enabled() -> bool {
292    std::env::var(RESIDENT_LATENCY_DIAGNOSTICS_ENV).as_deref() == Ok("1")
293}
294
295fn resident_latency_elapsed_ns(started: Option<std::time::Instant>) -> u64 {
296    started
297        .map(|started| u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX))
298        .unwrap_or(0)
299}
300
301fn resident_latency_unattributed_ns(total_ns: u64, phases: &[u64]) -> u64 {
302    phases.iter().copied().fold(total_ns, u64::saturating_sub)
303}
304
305/// Result of evaluating a single query in a Datalog program.
306pub struct LogicQueryResult {
307    /// Display relation name. Ordinary query projections use an internal name such as
308    /// `__xlog_query_0`; epistemic materializations use the source predicate name.
309    pub relation_name: String,
310    /// Output variable names in column order.
311    pub columns: Vec<String>,
312    /// Per-output-column sort labels in column order.
313    pub sort_labels: Vec<String>,
314    /// GPU-resident column buffer with the result tuples.
315    pub buffer: CudaBuffer,
316}
317
318/// Result of evaluating an entire Datalog program.
319pub struct LogicEvalResult {
320    /// One result per `?-` query in the source program.
321    pub queries: Vec<LogicQueryResult>,
322    /// Execution statistics (populated when profiling is enabled).
323    pub stats: Option<ExecutionStats>,
324}
325
326/// Runtime state retained by a persistent logic session.
327pub struct LogicSessionRuntime {
328    reusable_state_identity: Arc<LogicProgramIdentity>,
329    executor: Executor,
330    profiling: bool,
331}
332
333#[derive(Debug)]
334struct LogicProgramIdentity {
335    resident_certification:
336        OnceLock<std::result::Result<Arc<ResidentGraphCertifiedPlan>, Arc<str>>>,
337    #[cfg(test)]
338    resident_certification_initializations: AtomicU64,
339}
340
341impl LogicProgramIdentity {
342    fn new() -> Self {
343        Self {
344            resident_certification: OnceLock::new(),
345            #[cfg(test)]
346            resident_certification_initializations: AtomicU64::new(0),
347        }
348    }
349
350    fn get_or_init_resident_certification(
351        &self,
352        initialize: impl FnOnce() -> Result<ResidentGraphCertifiedPlan>,
353    ) -> Result<Arc<ResidentGraphCertifiedPlan>> {
354        let cached = self.resident_certification.get_or_init(|| {
355            #[cfg(test)]
356            self.resident_certification_initializations
357                .fetch_add(1, Ordering::Relaxed);
358            initialize()
359                .map(Arc::new)
360                .map_err(|error| Arc::<str>::from(error.to_string()))
361        });
362        cached.as_ref().map(Arc::clone).map_err(|message| {
363            XlogError::Execution(format!("resident route certification failed: {message}"))
364        })
365    }
366
367    fn get_or_init_resident_certification_with_outcome(
368        &self,
369        initialize: impl FnOnce() -> Result<ResidentGraphCertifiedPlan>,
370    ) -> Result<(Arc<ResidentGraphCertifiedPlan>, bool, bool)> {
371        let cache_was_warm = self.resident_certification.get().is_some();
372        let mut initialized_here = false;
373        let cached = self.resident_certification.get_or_init(|| {
374            initialized_here = true;
375            #[cfg(test)]
376            self.resident_certification_initializations
377                .fetch_add(1, Ordering::Relaxed);
378            initialize()
379                .map(Arc::new)
380                .map_err(|error| Arc::<str>::from(error.to_string()))
381        });
382        cached
383            .as_ref()
384            .map(|certified| (Arc::clone(certified), cache_was_warm, initialized_here))
385            .map_err(|message| {
386                XlogError::Execution(format!("resident route certification failed: {message}"))
387            })
388    }
389
390    #[cfg(test)]
391    fn resident_certification_initializations(&self) -> u64 {
392        self.resident_certification_initializations
393            .load(Ordering::Relaxed)
394    }
395}
396
397/// A materialized derived store produced by one compiled logic program.
398///
399/// The store's program identity is intentionally opaque. It can be inspected
400/// read-only, but only the originating [`LogicProgram`] (or one of its clones)
401/// can accept it as reusable execution state.
402pub struct LogicMaterializedStore {
403    reusable_state_identity: Arc<LogicProgramIdentity>,
404    store: RelationStore,
405}
406
407impl LogicMaterializedStore {
408    /// Borrow the materialized relations for read-only result inspection.
409    pub fn as_relation_store(&self) -> &RelationStore {
410        &self.store
411    }
412}
413
414impl LogicSessionRuntime {
415    /// Return persistent hash-index cache telemetry for the retained executor.
416    pub fn join_index_cache_stats(&self) -> JoinIndexCacheStats {
417        self.executor.join_index_cache_stats()
418    }
419
420    /// Return multiway/Free-Join dispatch telemetry for the retained executor.
421    pub fn wcoj_dispatch_stats(&self) -> WcojDispatchStats {
422        WcojDispatchStats {
423            free_join_dispatch_count: self.executor.free_join_dispatch_count(),
424            factorized_delta_dispatch_count: self.executor.factorized_delta_dispatch_count(),
425            wcoj_groupby_fusion_dispatch_count: self.executor.wcoj_groupby_fusion_dispatch_count(),
426            wcoj_error_decline_count: self.executor.wcoj_error_decline_count(),
427        }
428    }
429}
430
431/// Multiway/Free-Join dispatch telemetry counters for a retained session
432/// executor. Counts accumulate across evaluates within the session.
433#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
434pub struct WcojDispatchStats {
435    /// Free Join dispatches taken through the multiway plan.
436    pub free_join_dispatch_count: u64,
437    /// Factorized recursive-delta dispatches taken in the semi-naive
438    /// fixpoint (dense bitvector or sparse hash-set route).
439    pub factorized_delta_dispatch_count: u64,
440    /// Aggregate-fused group-by-root dispatches (no materialized join rows).
441    pub wcoj_groupby_fusion_dispatch_count: u64,
442    /// WCOJ pipeline errors that declined to the binary-join fallback.
443    pub wcoj_error_decline_count: u64,
444}
445
446/// Planner-grade telemetry for a persistent-session relation delta update.
447#[derive(Clone, Debug, Default, PartialEq)]
448pub struct DeltaPlannerTelemetry {
449    /// True when the relation-delta path reused an existing session/cache.
450    pub cache_reused: bool,
451    /// Planner decision used for this delta update.
452    pub fallback_decision: String,
453    /// Number of SCCs affected by the delta dependency closure.
454    pub affected_sccs: usize,
455    /// Number of SCCs recomputed from scratch.
456    pub recomputed_sccs: usize,
457    /// Number of SCCs updated incrementally.
458    pub incremental_sccs: usize,
459    /// Estimated speedup of delta evaluation over full recompute when available.
460    pub estimated_delta_speedup: Option<f64>,
461    /// Measured speedup of delta evaluation over full recompute when both timings are available.
462    pub measured_delta_speedup: Option<f64>,
463    /// Human-readable planner guidance for downstream diagnostics.
464    pub planner_advice: Vec<String>,
465}
466
467impl DeltaPlannerTelemetry {
468    /// Build planner telemetry from a delta report and optional timing evidence.
469    pub fn from_delta_report(
470        report: &LogicDeltaReport,
471        cache_reused: bool,
472        measured_micros: Option<(u64, u64)>,
473    ) -> Self {
474        let fallback_decision = if report.affected_sccs == 0 {
475            "no_op"
476        } else if report.has_deletes || report.recomputed_sccs > 0 {
477            "full_recompute_fallback"
478        } else {
479            "incremental"
480        }
481        .to_string();
482        let estimated_delta_speedup = if report.affected_sccs > 0 {
483            Some((report.affected_sccs.max(1) as f64) / (report.incremental_sccs.max(1) as f64))
484        } else {
485            None
486        };
487        let measured_delta_speedup = measured_micros.and_then(|(delta_us, full_us)| {
488            if delta_us == 0 {
489                None
490            } else {
491                Some(full_us as f64 / delta_us as f64)
492            }
493        });
494
495        let mut planner_advice = Vec::new();
496        if fallback_decision == "full_recompute_fallback" {
497            planner_advice.push(
498                "full recompute fallback selected; inspect deletes or affected SCC fanout"
499                    .to_string(),
500            );
501        } else if let Some(speedup) = measured_delta_speedup {
502            if speedup >= 1.0 {
503                planner_advice.push(format!("delta path is faster by {speedup:.2}x"));
504            } else {
505                planner_advice.push(format!(
506                    "full recompute may be faster; delta measured {speedup:.2}x"
507                ));
508            }
509        } else if fallback_decision == "incremental" {
510            planner_advice.push(
511                "incremental delta path selected; run equivalence timing to measure speedup"
512                    .to_string(),
513            );
514        }
515
516        Self {
517            cache_reused,
518            fallback_decision,
519            affected_sccs: report.affected_sccs,
520            recomputed_sccs: report.recomputed_sccs,
521            incremental_sccs: report.incremental_sccs,
522            estimated_delta_speedup,
523            measured_delta_speedup,
524            planner_advice,
525        }
526    }
527}
528
529/// Summary for a persistent-session relation delta update.
530pub struct LogicDeltaReport {
531    /// Number of relation delta entries supplied by the caller before coalescing.
532    pub input_delta_count: usize,
533    /// Number of changed relation names in the delta batch.
534    pub changed_relations: usize,
535    /// Changed relation names after coalescing.
536    pub changed_relation_names: Vec<String>,
537    /// Total inserted rows across all changed relations.
538    pub insert_rows: u64,
539    /// Total deleted rows across all changed relations.
540    pub delete_rows: u64,
541    /// True when at least one relation supplied delete rows.
542    pub has_deletes: bool,
543    /// Number of SCCs whose dependency closure was affected.
544    pub affected_sccs: usize,
545    /// Number of affected SCCs that were cleared and fully recomputed.
546    pub recomputed_sccs: usize,
547    /// Number of affected SCCs updated without clearing prior output.
548    pub incremental_sccs: usize,
549    /// Net insert rows after batch coalescing and insert/delete cancellation.
550    pub coalesced_insert_rows: u64,
551    /// Net delete rows after batch coalescing and insert/delete cancellation.
552    pub coalesced_delete_rows: u64,
553    /// Rows canceled because an insert and delete for the same relation matched in the batch.
554    pub canceled_rows: u64,
555    /// Planner-grade cache, fallback, and speedup telemetry.
556    pub planner_telemetry: DeltaPlannerTelemetry,
557    /// Metadata-only debug trace for the delta recompute.
558    pub debug_trace: Vec<String>,
559}
560
561/// Direction of an incoming relation-delta occurrence.
562#[derive(Clone, Copy, Debug, PartialEq, Eq)]
563pub enum RelationDeltaDirection {
564    /// Tuples supplied on the insertion side of an update.
565    Insert,
566    /// Tuples supplied on the deletion side of an update.
567    Delete,
568}
569
570/// Tuples canceled at one ordered merge step of a relation-delta batch.
571pub struct RelationDeltaCancellation {
572    update_index: usize,
573    incoming_direction: RelationDeltaDirection,
574    tuples: CudaBuffer,
575}
576
577impl RelationDeltaCancellation {
578    /// Return the zero-based position of the incoming update in the original batch.
579    pub fn update_index(&self) -> usize {
580        self.update_index
581    }
582
583    /// Return the direction of the incoming occurrence that caused the cancellation.
584    pub fn incoming_direction(&self) -> RelationDeltaDirection {
585        self.incoming_direction
586    }
587
588    /// Borrow the GPU-resident tuples canceled at this merge step.
589    pub fn tuples(&self) -> &CudaBuffer {
590        &self.tuples
591    }
592}
593
594#[derive(Clone, Copy)]
595struct PreparedRelationDeltaReportSeed {
596    input_delta_count: usize,
597    changed_relations: usize,
598    coalesced_insert_rows: u64,
599    coalesced_delete_rows: u64,
600    canceled_rows: u64,
601}
602
603/// Device-coalesced relation updates prepared for validation and later application.
604///
605/// Dropping this value releases its GPU-resident net-delta and cancellation
606/// buffers without changing any relation store.
607#[must_use = "prepared relation deltas have no effect until they are committed"]
608pub struct PreparedRelationDeltaBatch {
609    deltas: HashMap<String, RelationDelta>,
610    cancellations: HashMap<String, Vec<RelationDeltaCancellation>>,
611    report_seed: PreparedRelationDeltaReportSeed,
612}
613
614impl PreparedRelationDeltaBatch {
615    /// Borrow the final net relation deltas produced by the device coalescer.
616    pub fn net_deltas(&self) -> &HashMap<String, RelationDelta> {
617        &self.deltas
618    }
619
620    /// Borrow per-relation cancellation traces in global update order.
621    pub fn cancellations(&self) -> &HashMap<String, Vec<RelationDeltaCancellation>> {
622        &self.cancellations
623    }
624
625    fn into_application_parts(
626        self,
627    ) -> (
628        HashMap<String, RelationDelta>,
629        PreparedRelationDeltaReportSeed,
630    ) {
631        (self.deltas, self.report_seed)
632    }
633}
634
635/// A fully staged relation update bound to its authoritative and derived state.
636///
637/// The exclusive borrows prevent callers from mutating or substituting the
638/// authoritative store, cache, or runtime between preparation and commit.
639/// Dropping this value discards every staged update and prospective derived
640/// state. The authoritative store remains unchanged, while the borrowed cache
641/// and runtime slots remain empty because their prior values were consumed by
642/// preparation.
643///
644/// A prepared commit has no API for selecting a different destination:
645///
646/// ```compile_fail
647/// use xlog_gpu::logic::PreparedRelationDeltaCommit;
648/// use xlog_runtime::RelationStore;
649///
650/// fn commit_into_another_store(
651///     prepared: PreparedRelationDeltaCommit<'_>,
652///     other: &mut RelationStore,
653/// ) {
654///     prepared.commit(other);
655/// }
656/// ```
657///
658/// The source store also remains exclusively borrowed until commit:
659///
660/// ```compile_fail
661/// use std::sync::Arc;
662/// use xlog_core::Result;
663/// use xlog_cuda::{CudaBuffer, CudaKernelProvider};
664/// use xlog_gpu::logic::{
665///     LogicMaterializedStore, LogicProgram, LogicSessionRuntime, PreparedRelationDeltaBatch,
666/// };
667/// use xlog_runtime::RelationStore;
668///
669/// fn mutate_after_prepare(
670///     program: &LogicProgram,
671///     provider: Arc<CudaKernelProvider>,
672///     store: &mut RelationStore,
673///     cache: &mut Option<LogicMaterializedStore>,
674///     runtime: &mut Option<LogicSessionRuntime>,
675///     batch: PreparedRelationDeltaBatch,
676///     replacement: CudaBuffer,
677/// ) -> Result<()> {
678///     let prepared = program.prepare_relation_delta_commit_with_session_runtime(
679///         provider, store, cache, runtime, batch,
680///     )?;
681///     store.put("fact", replacement);
682///     prepared.commit();
683///     Ok(())
684/// }
685/// ```
686#[must_use = "dropping a prepared commit discards its staged relation updates"]
687pub struct PreparedRelationDeltaCommit<'a> {
688    provider: Arc<CudaKernelProvider>,
689    authoritative_relation_store: &'a mut RelationStore,
690    cached_store_slot: &'a mut Option<LogicMaterializedStore>,
691    session_runtime_slot: &'a mut Option<LogicSessionRuntime>,
692    staged_base_updates: Vec<(String, CudaBuffer)>,
693    prospective_cached_store: Option<LogicMaterializedStore>,
694    prospective_session_runtime: Option<LogicSessionRuntime>,
695    report: LogicDeltaReport,
696}
697
698impl PreparedRelationDeltaCommit<'_> {
699    /// Borrow the prospective materialized derived/cache store.
700    ///
701    /// This store is suitable for direct query-result comparison. It must not
702    /// seed an independent full recompute because it contains intensional heads
703    /// that an executor may union with newly derived rows. For a no-op batch it
704    /// returns retained derived state when available, or the unchanged
705    /// authoritative store otherwise.
706    pub fn prospective_derived_store(&self) -> &RelationStore {
707        if let Some(store) = self.prospective_cached_store.as_ref() {
708            return &store.store;
709        }
710        if let Some(runtime) = self.prospective_session_runtime.as_ref() {
711            return runtime.executor.store();
712        }
713        &*self.authoritative_relation_store
714    }
715
716    /// Clone the authoritative base snapshot with every staged base update overlaid.
717    ///
718    /// This fallible, on-demand snapshot is the correct seed for an independent
719    /// full recompute. It includes staged relations that were absent from the
720    /// authoritative store and does not mutate authoritative contents or
721    /// versions. Ordinary prepare and commit paths do not pay for these clones.
722    pub fn clone_prospective_base_store(&self) -> Result<RelationStore> {
723        let mut authoritative_names = self
724            .authoritative_relation_store
725            .names()
726            .filter(|name| {
727                !self
728                    .staged_base_updates
729                    .iter()
730                    .any(|(staged_name, _)| staged_name == name)
731            })
732            .collect::<Vec<_>>();
733        authoritative_names.sort_unstable();
734        let mut cloned = RelationStore::new(self.provider.clone());
735        cloned.try_reserve_relations(authoritative_names.len() + self.staged_base_updates.len())?;
736
737        for name in authoritative_names {
738            let buffer = self.authoritative_relation_store.get(name).ok_or_else(|| {
739                XlogError::Execution(format!(
740                    "Authoritative relation {name} disappeared while cloning prospective base state"
741                ))
742            })?;
743            let context = format!("cloning prospective base relation '{name}'");
744            let cloned_buffer = self
745                .provider
746                .clone_buffer(buffer)
747                .map_err(|error| relation_clone_error(context, error))?;
748            cloned.put(name, cloned_buffer);
749        }
750
751        for (name, buffer) in &self.staged_base_updates {
752            let context = format!("cloning staged prospective base relation '{name}'");
753            let cloned_buffer = self
754                .provider
755                .clone_buffer(buffer)
756                .map_err(|error| relation_clone_error(context, error))?;
757            cloned.put(name, cloned_buffer);
758        }
759        Ok(cloned)
760    }
761
762    /// Install every staged base update and derived-state replacement together.
763    ///
764    /// All allocation, destination-capacity reservation, recomputation,
765    /// constraint validation, and buffer cloning has completed before this
766    /// method is available, so committing only moves owned values and is
767    /// infallible.
768    pub fn commit(self) -> LogicDeltaReport {
769        for (name, buffer) in self.staged_base_updates {
770            self.authoritative_relation_store.put_owned(name, buffer);
771        }
772        *self.cached_store_slot = self.prospective_cached_store;
773        *self.session_runtime_slot = self.prospective_session_runtime;
774        self.report
775    }
776}
777
778#[derive(Default)]
779struct PendingRelationDelta {
780    insert: Option<CudaBuffer>,
781    delete: Option<CudaBuffer>,
782}
783
784/// One stratum of a stratified epistemic plan: the epistemic head(s) it
785/// materializes plus the GPU executable plan that computes them.
786///
787/// Lower strata are executed first; their GATED head outputs are written into the
788/// relation store as base relations BEFORE higher strata run, so a higher
789/// stratum's `know`/`possible` over a lower head gates against the materialized
790/// (now-base) relation through the existing tuple-key membership filter.
791#[derive(Clone)]
792struct StratumExecutable {
793    /// The stratum's GPU plan: single-head or joint multi-head split. The gated
794    /// head relation name(s) are recovered from the plan's reductions at runtime.
795    plan: StratumPlanKind,
796}
797
798#[derive(Clone)]
799struct StratifiedExecutable {
800    strata: Vec<StratumExecutable>,
801    /// Single ordinary closure and authored-constraint stage executed only after
802    /// every modal stratum has materialized its gated heads.
803    ordinary_post: GpuOrdinaryPass,
804}
805
806#[derive(Clone)]
807enum StratumPlanKind {
808    Single(Box<EpistemicExecutablePlan>),
809    Split(Box<EpistemicSplitExecutablePlan>),
810    /// A higher stratum that RECURSES over a lower stratum's materialized
811    /// (now-base) determined head. Once the determined head is a base relation in
812    /// the store, its `know`/`possible` modal is over an invariant relation, so the
813    /// stratum is admissible Case-A: the modal resolves to an ordinary join (no
814    /// second gate) and the recursive semi-naive engine iterates the fixpoint. The
815    /// reduced ordinary program drives an ordinary RIR plan whose head IS this
816    /// stratum's user-visible output relation.
817    Ordinary {
818        plan: Box<ExecutionPlan>,
819        /// User-visible output head predicate(s) this stratum computes.
820        head_predicates: Vec<String>,
821    },
822}
823
824#[derive(Clone)]
825enum LogicExecutionPlan {
826    Ordinary(Box<ExecutionPlan>),
827    EpistemicG91Compatibility(Box<EpistemicG91CompatibilityGpuPlan>),
828    EpistemicWfsGpu(Box<EpistemicWfsGpuPlan>),
829    EpistemicSingle(Box<EpistemicExecutablePlan>),
830    EpistemicSplit(Box<EpistemicSplitExecutablePlan>),
831    /// Stratified epistemic execution: ordered strata, each materializing its
832    /// gated head(s) into the store before the next stratum runs.
833    EpistemicStratified(Box<StratifiedExecutable>),
834}
835
836#[derive(Clone)]
837struct EpistemicG91CompatibilityGpuPlan {
838    upper_bound: GpuEvaluationPass,
839    refinement: GpuEvaluationPass,
840    snapshot_relations: BTreeMap<String, String>,
841    convergence_predicates: Vec<String>,
842    max_iterations: usize,
843}
844
845#[derive(Clone)]
846enum GpuEvaluationPass {
847    Ordinary(Box<GpuOrdinaryPass>),
848    Wfs(Box<EpistemicWfsGpuPlan>),
849}
850
851#[derive(Clone)]
852struct EpistemicWfsGpuPlan {
853    overapprox: GpuOrdinaryPass,
854    lower: GpuOrdinaryPass,
855    upper: GpuOrdinaryPass,
856    intensional_predicates: Vec<String>,
857    upper_fixed_names: HashMap<String, String>,
858    lower_fixed_names: HashMap<String, String>,
859    max_iterations: usize,
860}
861
862#[derive(Clone)]
863struct GpuOrdinaryPass {
864    plan: ExecutionPlan,
865    schemas: HashMap<String, Schema>,
866    rel_ids: HashMap<String, RelId>,
867}
868
869/// Compile-time epistemic provenance, retained even when the executable plan is
870/// `Ordinary` (e.g. a Case-A recursive epistemic fixpoint whose modal literals were
871/// resolved into invariant joins). This carries the source's epistemic literals so
872/// the epistemic plan dump can emit a stable id for a recursive epistemic fixpoint that no
873/// longer carries an epistemic GPU plan.
874#[derive(Clone)]
875struct EpistemicProvenance {
876    /// How the epistemic source was reduced for execution.
877    reduction: &'static str,
878    /// Epistemic `know`/`possible` literals (with negation) seen in the source EIR.
879    literals: Vec<xlog_ir::EirEpistemicLiteral>,
880    /// Whether ordinary-plan query results retain the source epistemic relation name
881    /// and logical zero-column shape instead of exposing compiler projection details.
882    surface_source_queries: bool,
883}
884
885/// A compiled Datalog program ready for GPU evaluation.
886#[derive(Clone)]
887pub struct LogicProgram {
888    reusable_state_identity: Arc<LogicProgramIdentity>,
889    /// Merged authored program retained for public diagnostics and result labels.
890    source_program: Program,
891    /// Normalized or reduced program used by compilation and execution.
892    program: Program,
893    /// Integrity constraints as authored before normalization, retained only when
894    /// their order matches the source-order constraint provenance used by the plan.
895    authored_constraints: Option<Vec<Constraint>>,
896    plan: LogicExecutionPlan,
897    schemas: HashMap<String, Schema>,
898    rel_ids: HashMap<String, RelId>,
899    /// `Some` iff the source program contained epistemic literals (regardless of
900    /// whether the executable plan ended up epistemic or ordinary).
901    epistemic_provenance: Option<EpistemicProvenance>,
902}
903
904/// Read-only metadata for one argument of a compiled relation.
905#[derive(Clone, Debug, PartialEq, Eq)]
906pub struct LogicArgumentSchema {
907    name: String,
908    source_named: bool,
909    sort: Option<String>,
910    scalar_type: ScalarType,
911}
912
913impl LogicArgumentSchema {
914    /// Return the compiled column name.
915    pub fn name(&self) -> &str {
916        &self.name
917    }
918
919    /// Return whether the column name was written in the source declaration.
920    pub fn source_named(&self) -> bool {
921        self.source_named
922    }
923
924    /// Return the source domain alias, when the argument used one.
925    pub fn sort(&self) -> Option<&str> {
926        self.sort.as_deref()
927    }
928
929    /// Return the resolved scalar type from the compiled relation schema.
930    pub fn scalar_type(&self) -> ScalarType {
931        self.scalar_type
932    }
933}
934
935impl LogicProgram {
936    /// Compile a Datalog source string into a GPU-executable program.
937    pub fn compile(source: &str) -> Result<Self> {
938        let program = xlog_logic::parse_program(source)?;
939        Self::compile_program(program)
940    }
941
942    /// Compile an already parsed program into a GPU-executable program.
943    ///
944    /// This method does not resolve imports; import-aware callers merge them
945    /// first. The program enters the canonical execution normalizer once here.
946    pub fn compile_program(mut program: Program) -> Result<Self> {
947        if program.authored_constraint_source_bound.is_some() {
948            program.validate_prepared_authored_constraint_identity()?;
949        } else {
950            program.prepare_authored_constraint_identity_at_root()?;
951        }
952        let source_program = program.clone();
953        let normalized = normalize_program_for_execution(program)?;
954        Self::compile_normalized_program(normalized, source_program)
955    }
956
957    fn compile_normalized_program(normalized: Program, source_program: Program) -> Result<Self> {
958        for query_index in 0..normalized.queries.len() {
959            let generated_head = format!("__xlog_query_{query_index}");
960            let authored_collision = source_program
961                .predicates
962                .iter()
963                .any(|declaration| declaration.name == generated_head)
964                || source_program
965                    .rules
966                    .iter()
967                    .any(|rule| rule.head.predicate == generated_head);
968            if authored_collision {
969                return Err(XlogError::Compilation(format!(
970                    "authored relation {generated_head} collides with generated query head"
971                )));
972            }
973        }
974        // Function, meta-term, list, and shared-variable normalization preserve
975        // constraint count and source order. Keep the authored snapshot only
976        // while that one-to-one invariant remains observable.
977        let authored_constraints = (source_program.constraints.len()
978            == normalized.constraints.len())
979        .then(|| source_program.constraints.clone());
980        let reusable_state_identity = Arc::new(LogicProgramIdentity::new());
981        let compiled = if program_has_epistemic_literals(&normalized) {
982            Self::compile_epistemic_program(
983                normalized,
984                source_program,
985                authored_constraints,
986                reusable_state_identity,
987            )?
988        } else {
989            let mut compiler = Compiler::new();
990            let plan = match qualify_same_name_multi_arity_program(&normalized) {
991                Some(qualified) => compiler.compile_prepared_program(&qualified)?,
992                None => compiler.compile_prepared_program(&normalized)?,
993            };
994            let mut schemas = compiler.schemas().clone();
995            augment_same_name_multi_arity_schemas(&normalized, &mut schemas)?;
996            Self {
997                reusable_state_identity,
998                source_program,
999                program: normalized,
1000                authored_constraints,
1001                plan: LogicExecutionPlan::Ordinary(Box::new(plan)),
1002                schemas,
1003                rel_ids: compiler.rel_ids().clone(),
1004                epistemic_provenance: None,
1005            }
1006        };
1007        Ok(compiled.finalize_compilation())
1008    }
1009
1010    fn finalize_compilation(self) -> Self {
1011        if !self.program.queries.is_empty() {
1012            if let LogicExecutionPlan::Ordinary(plan) = &self.plan {
1013                let _ = self.resident_certified_plan_for_plan(plan);
1014            }
1015        }
1016        self
1017    }
1018
1019    fn validate_reusable_state_identity(
1020        &self,
1021        state_identity: &Arc<LogicProgramIdentity>,
1022        state_name: &str,
1023    ) -> Result<()> {
1024        if Arc::ptr_eq(&self.reusable_state_identity, state_identity) {
1025            return Ok(());
1026        }
1027        Err(XlogError::Execution(format!(
1028            "{state_name} belongs to a different compiled logic program"
1029        )))
1030    }
1031
1032    fn validate_reusable_state_slots(
1033        &self,
1034        cached_store: Option<&LogicMaterializedStore>,
1035        session_runtime: Option<&LogicSessionRuntime>,
1036    ) -> Result<()> {
1037        if let Some(cached_store) = cached_store {
1038            self.validate_reusable_state_identity(
1039                &cached_store.reusable_state_identity,
1040                "materialized cache",
1041            )?;
1042        }
1043        if let Some(session_runtime) = session_runtime {
1044            self.validate_reusable_state_identity(
1045                &session_runtime.reusable_state_identity,
1046                "session runtime",
1047            )?;
1048        }
1049        Ok(())
1050    }
1051
1052    fn bind_materialized_store(&self, store: RelationStore) -> LogicMaterializedStore {
1053        LogicMaterializedStore {
1054            reusable_state_identity: self.reusable_state_identity.clone(),
1055            store,
1056        }
1057    }
1058
1059    fn compile_epistemic_program(
1060        normalized: Program,
1061        source_program: Program,
1062        authored_constraints: Option<Vec<Constraint>>,
1063        reusable_state_identity: Arc<LogicProgramIdentity>,
1064    ) -> Result<Self> {
1065        // Capture epistemic provenance up front: the source-EIR modal literals are
1066        // retained even when a Case-A recursive reduction lowers the program to an
1067        // Ordinary executable plan, so the epistemic plan dump can still emit a stable id
1068        // for a recursive epistemic fixpoint.
1069        let provenance_literals = collect_eir_epistemic_literals(&normalized);
1070        let prepared = prepare_epistemic_program(&normalized)?;
1071        let active_program = prepared.active_program();
1072
1073        // Positive Gelfond-1991 `possible` cycles require tuple-level dynamic
1074        // compatibility. Predicate SCC membership alone cannot prove that the same
1075        // concrete tuple survives every edge's ordinary body filters. Route the
1076        // complete active program through a descending GPU fixpoint before any
1077        // stratified or ordinary least-fixpoint reduction can erase those gates.
1078        if let Some(reduction) = try_prepare_g91_compatibility_reduction(&prepared)? {
1079            let plan = compile_g91_compatibility_gpu_plan(&reduction)?;
1080            let schemas = g91_plan_combined_schemas(&plan);
1081            let rel_ids = g91_plan_combined_rel_ids(&plan);
1082            return Ok(Self {
1083                reusable_state_identity,
1084                source_program,
1085                program: reduction.refinement_program().clone(),
1086                authored_constraints,
1087                plan: LogicExecutionPlan::EpistemicG91Compatibility(Box::new(plan)),
1088                schemas,
1089                rel_ids,
1090                epistemic_provenance: Some(EpistemicProvenance {
1091                    reduction: "g91_tuple_compatibility",
1092                    literals: provenance_literals,
1093                    surface_source_queries: true,
1094                }),
1095            });
1096        }
1097
1098        // Stratified epistemic execution FIRST: a modal literal ranges over an
1099        // epistemically-DETERMINED derived head (`b :- know a` where `a :- know p`,
1100        // `p` invariant — possibly with the higher stratum RECURSING over the
1101        // determined head, e.g. `reach :- reach, know a`). Partition into strata;
1102        // each is compiled through the existing epistemic OR Case-A ordinary path,
1103        // and at runtime each lower stratum's GATED head is materialized into the
1104        // store as a base relation before the higher stratum gates against it (via
1105        // the existing tuple-key membership filter or — once the head is a materialized
1106        // base relation — Case-A resolve-into-body; either way NO double-gating
1107        // against a still-modal relation). A shared BASE modal `q` (EDB, not a
1108        // determined derived head) returns `None` here and falls through to
1109        // the joint path UNCHANGED; plain Case-A recursion over an EDB modal
1110        // (`know edge`) also returns `None` and falls through to Case-A below.
1111        if let Some(stratified) = try_plan_stratified_epistemic_program(active_program)? {
1112            // SCHEMA-ONLY reduction: resolve augmenting positive modals over INVARIANT
1113            // *or* epistemically-DETERMINED targets into positive ordinary atoms, so an
1114            // augmented head whose extra output column is bound by a modal over a
1115            // multi-column determined head (`out(X) :- node(X), know r(X, Y)`, `r`
1116            // determined) types its appended `Y` column from `r`'s declaration instead
1117            // of failing closed as `UnsafeVariable`. This drives ONLY plan schema
1118            // inference; per-stratum EXECUTION compiles below over sub-programs where
1119            // the determined head is already a materialized base relation (strict
1120            // invariant resolve), so no modal is ever resolved over an un-gated
1121            // candidate at runtime.
1122            let reduced =
1123                reduce_epistemic_program_to_ordinary_for_stratified_schema(active_program)?;
1124            let mut schema_compiler = Compiler::new();
1125            schema_compiler.compile_prepared_program(&reduced)?;
1126            let mut schemas = schema_compiler.schemas().clone();
1127            augment_same_name_multi_arity_schemas(active_program, &mut schemas)?;
1128
1129            let mut strata = Vec::with_capacity(stratified.strata.len());
1130            for stratum in &stratified.strata {
1131                strata.push(StratumExecutable {
1132                    plan: Self::compile_stratum_plan(&stratum.program)?,
1133                });
1134            }
1135            let ordinary_post = compile_gpu_ordinary_pass(&stratified.ordinary_post_program)?;
1136            for (name, schema) in &ordinary_post.schemas {
1137                schemas
1138                    .entry(name.clone())
1139                    .or_insert_with(|| schema.clone());
1140            }
1141            let plan = LogicExecutionPlan::EpistemicStratified(Box::new(StratifiedExecutable {
1142                strata,
1143                ordinary_post,
1144            }));
1145            let rel_ids = epistemic_relation_ids(&plan)?;
1146            return Ok(Self {
1147                reusable_state_identity,
1148                source_program,
1149                program: normalized,
1150                authored_constraints,
1151                plan,
1152                schemas,
1153                rel_ids,
1154                epistemic_provenance: Some(EpistemicProvenance {
1155                    reduction: "stratified",
1156                    literals: provenance_literals,
1157                    surface_source_queries: true,
1158                }),
1159            });
1160        }
1161
1162        // Case A/B: reduce admissible recursive epistemic programs to ordinary
1163        // recursion. Stratified reduced programs route through the existing ordinary
1164        // semi-naive engine; non-monotone reduced SCCs route through the GPU-native
1165        // WFS alternating-fixpoint plan below. Recursive shapes outside the admissible
1166        // fragment still fail closed in `try_reduce_case_a_recursive_epistemic_program`.
1167        if let Some(recursive_reduced) = try_reduce_prepared_recursive_epistemic_program(&prepared)?
1168        {
1169            let strat = xlog_logic::stratify::analyze_stratification(&recursive_reduced);
1170            if !strat.non_monotone_sccs.is_empty() {
1171                let wfs_plan = compile_epistemic_wfs_gpu_plan(&recursive_reduced)?;
1172                let schemas = wfs_plan_combined_schemas(&wfs_plan);
1173                let rel_ids = wfs_plan_combined_rel_ids(&wfs_plan);
1174                return Ok(Self {
1175                    reusable_state_identity,
1176                    source_program,
1177                    program: recursive_reduced,
1178                    authored_constraints,
1179                    plan: LogicExecutionPlan::EpistemicWfsGpu(Box::new(wfs_plan)),
1180                    schemas,
1181                    rel_ids,
1182                    epistemic_provenance: Some(EpistemicProvenance {
1183                        reduction: "wfs_gpu_recursive",
1184                        literals: provenance_literals,
1185                        surface_source_queries: true,
1186                    }),
1187                });
1188            }
1189            let mut compiler = Compiler::new();
1190            let plan = compiler.compile_prepared_program(&recursive_reduced)?;
1191            return Ok(Self {
1192                reusable_state_identity,
1193                source_program,
1194                program: recursive_reduced,
1195                authored_constraints,
1196                plan: LogicExecutionPlan::Ordinary(Box::new(plan)),
1197                schemas: compiler.schemas().clone(),
1198                rel_ids: compiler.rel_ids().clone(),
1199                epistemic_provenance: Some(EpistemicProvenance {
1200                    reduction: "ordinary_recursive_modal_reduction",
1201                    literals: provenance_literals,
1202                    surface_source_queries: true,
1203                }),
1204            });
1205        }
1206
1207        let reduced = reduce_epistemic_program_to_ordinary(active_program)?;
1208        let mut schema_compiler = Compiler::new();
1209        schema_compiler.compile_prepared_program(&reduced)?;
1210        let mut schemas = schema_compiler.schemas().clone();
1211        augment_same_name_multi_arity_schemas(active_program, &mut schemas)?;
1212
1213        let plan = if epistemic_output_head_predicate_count(active_program) > 1 {
1214            LogicExecutionPlan::EpistemicSplit(Box::new(compile_epistemic_gpu_split_execution(
1215                active_program,
1216            )?))
1217        } else {
1218            match compile_epistemic_gpu_execution(active_program) {
1219                Ok(executable) => LogicExecutionPlan::EpistemicSingle(Box::new(executable)),
1220                Err(XlogError::UnsupportedEpistemicConstruct { construct, .. })
1221                    if construct == "epistemic GPU final output relation" =>
1222                {
1223                    LogicExecutionPlan::EpistemicSplit(Box::new(
1224                        compile_epistemic_gpu_split_execution(active_program)?,
1225                    ))
1226                }
1227                Err(err) => return Err(err),
1228            }
1229        };
1230        let rel_ids = epistemic_relation_ids(&plan)?;
1231        Ok(Self {
1232            reusable_state_identity,
1233            source_program,
1234            program: normalized,
1235            authored_constraints,
1236            plan,
1237            schemas,
1238            rel_ids,
1239            epistemic_provenance: Some(EpistemicProvenance {
1240                reduction: "epistemic_executable",
1241                literals: provenance_literals,
1242                surface_source_queries: false,
1243            }),
1244        })
1245    }
1246
1247    /// Compile one stratum sub-program into its plan kind.
1248    ///
1249    /// A stratum whose epistemic heads gate only over invariant or
1250    /// already-materialized lower-stratum relations is either an admissible Case-A
1251    /// recursion (the modal resolves to an ordinary join over the now-base relation)
1252    /// or a plain single/joint epistemic plan. Case-A is tried first so a recursive
1253    /// higher stratum (`reach :- reach, know a`, `a` materialized base) routes
1254    /// through the ordinary semi-naive engine.
1255    fn compile_stratum_plan(stratum_program: &Program) -> Result<StratumPlanKind> {
1256        if let Some(case_a_reduced) =
1257            try_reduce_case_a_recursive_epistemic_program(stratum_program)?
1258        {
1259            let mut compiler = Compiler::new();
1260            let plan = compiler.compile_prepared_program(&case_a_reduced)?;
1261            let head_predicates = epistemic_stratum_output_heads(stratum_program);
1262            return Ok(StratumPlanKind::Ordinary {
1263                plan: Box::new(plan),
1264                head_predicates,
1265            });
1266        }
1267        if epistemic_output_head_predicate_count(stratum_program) > 1 {
1268            Ok(StratumPlanKind::Split(Box::new(
1269                compile_epistemic_gpu_split_execution(stratum_program)?,
1270            )))
1271        } else {
1272            Ok(StratumPlanKind::Single(Box::new(
1273                compile_epistemic_gpu_execution(stratum_program)?,
1274            )))
1275        }
1276    }
1277
1278    /// Compile a program with module resolution.
1279    ///
1280    /// This method resolves all imports using the provided resolver and merges
1281    /// imported predicates, functions, and rules into the main program.
1282    ///
1283    /// Pragmas are entry-file-scoped: directives declared in imported modules
1284    /// are dropped at merge time. This library entry point does not surface
1285    /// them — embedders that want the CLI's `warning[W0510]` behavior should
1286    /// call `resolver.ignored_import_pragmas()` before compiling and report
1287    /// the returned records on their own diagnostics channel.
1288    ///
1289    /// # Arguments
1290    /// * `source` - The source code of the main program
1291    /// * `resolver` - A pre-loaded ModuleResolver with all dependencies resolved
1292    ///
1293    /// # Returns
1294    /// The compiled LogicProgram with all imports merged
1295    pub fn compile_with_resolver(
1296        source: &str,
1297        resolver: &xlog_logic::resolver::ModuleResolver,
1298    ) -> Result<Self> {
1299        let program = xlog_logic::parse_program(source)?;
1300
1301        // Merge imports from the resolver
1302        let merged = resolver
1303            .merge_imports(program)
1304            .map_err(|e| XlogError::Compilation(format!("Module resolution failed: {}", e)))?;
1305
1306        Self::compile_program(merged)
1307    }
1308
1309    /// Serialize the compiled epistemic execution plan to a JSON summary.
1310    ///
1311    /// Returns `None` for ordinary (non-epistemic) programs. For epistemic
1312    /// programs this dumps the EIR-derived GPU plan(s): selected mode, the
1313    /// epistemic `know`/`possible` literals (with negation), required GPU hot-path
1314    /// phases/kernels, world-view integrity constraints, reduced-program head
1315    /// summaries, the fail-closed GPU execution policy, and a deterministic plan
1316    /// id (a stable hash of the canonical summary). Runtime evidence separately
1317    /// records observed dispatch, kernel, device-buffer, candidate-accounting,
1318    /// solver/probability event, and scoped-transfer behavior.
1319    pub fn epistemic_plan_json(&self) -> Option<String> {
1320        let mut has_ordinary_post = false;
1321        let gpu_plans: Vec<(String, &xlog_ir::EpistemicGpuPlan)> = match &self.plan {
1322            // A program whose source was epistemic but whose executable plan is
1323            // ordinary either resolved admissible recursive modal literals into joins
1324            // or removed every unfounded FAEEL modal rule. It carries no epistemic GPU
1325            // plan and executes through the ordinary GPU engine under the same
1326            // reject-unsupported policy. Emit a provenance summary with a stable id so the reduction is
1327            // auditable.
1328            LogicExecutionPlan::Ordinary(_) => {
1329                let prov = self.epistemic_provenance.as_ref()?;
1330                return Some(epistemic_provenance_summary_json(
1331                    "epistemic_reduced_ordinary",
1332                    prov,
1333                    None,
1334                    None,
1335                ));
1336            }
1337            LogicExecutionPlan::EpistemicWfsGpu(wfs) => {
1338                let prov = self.epistemic_provenance.as_ref()?;
1339                return Some(epistemic_provenance_summary_json(
1340                    self.plan_kind_label(),
1341                    prov,
1342                    Some(wfs.max_iterations),
1343                    Some(wfs),
1344                ));
1345            }
1346            LogicExecutionPlan::EpistemicG91Compatibility(g91) => {
1347                let prov = self.epistemic_provenance.as_ref()?;
1348                return Some(g91_compatibility_summary_json(
1349                    self.plan_kind_label(),
1350                    prov,
1351                    g91,
1352                ));
1353            }
1354            LogicExecutionPlan::EpistemicSingle(plan) => {
1355                vec![("single".to_string(), &plan.gpu_plan)]
1356            }
1357            LogicExecutionPlan::EpistemicSplit(split) => split
1358                .components
1359                .iter()
1360                .enumerate()
1361                .map(|(i, c)| (format!("split[{i}]"), &c.executable.gpu_plan))
1362                .collect(),
1363            LogicExecutionPlan::EpistemicStratified(stratified) => {
1364                let mut plans = Vec::new();
1365                for (i, stratum) in stratified.strata.iter().enumerate() {
1366                    match &stratum.plan {
1367                        StratumPlanKind::Single(plan) => {
1368                            plans.push((format!("stratum[{i}]"), &plan.gpu_plan));
1369                        }
1370                        StratumPlanKind::Split(split) => {
1371                            for (j, c) in split.components.iter().enumerate() {
1372                                plans.push((
1373                                    format!("stratum[{i}].split[{j}]"),
1374                                    &c.executable.gpu_plan,
1375                                ));
1376                            }
1377                        }
1378                        // Recursive/ordinary higher strata carry no epistemic GPU
1379                        // plan (the modal already resolved to an ordinary join over
1380                        // a materialized base); the enclosing summary carries the
1381                        // same fail-closed GPU execution policy.
1382                        StratumPlanKind::Ordinary { .. } => {}
1383                    }
1384                }
1385                has_ordinary_post = true;
1386                plans
1387            }
1388        };
1389        Some(epistemic_plan_summary_json(
1390            self.plan_kind_label(),
1391            &gpu_plans,
1392            has_ordinary_post,
1393        ))
1394    }
1395
1396    fn plan_kind_label(&self) -> &'static str {
1397        match &self.plan {
1398            LogicExecutionPlan::Ordinary(_) => "ordinary",
1399            LogicExecutionPlan::EpistemicG91Compatibility(_) => "epistemic_g91_compatibility_gpu",
1400            LogicExecutionPlan::EpistemicWfsGpu(_) => "epistemic_wfs_gpu",
1401            LogicExecutionPlan::EpistemicSingle(_) => "epistemic_single",
1402            LogicExecutionPlan::EpistemicSplit(_) => "epistemic_split",
1403            LogicExecutionPlan::EpistemicStratified(_) => "epistemic_stratified",
1404        }
1405    }
1406
1407    /// Look up the schema for a named relation.
1408    pub fn schema(&self, relation: &str) -> Option<&Schema> {
1409        self.schemas.get(relation)
1410    }
1411
1412    /// Return the full schema map (relation name to schema).
1413    pub fn schemas(&self) -> &HashMap<String, Schema> {
1414        &self.schemas
1415    }
1416
1417    /// Return ordered argument metadata for a compiled relation.
1418    ///
1419    /// Column names and scalar types come from the compiled schema. Source
1420    /// declarations additionally preserve whether each name was explicit and
1421    /// which domain alias, if any, supplied its scalar type.
1422    pub fn argument_schema(&self, relation: &str) -> Option<Vec<LogicArgumentSchema>> {
1423        let schema = self.schemas.get(relation)?;
1424        let presentation_program = self.presentation_program();
1425        let source_declaration = presentation_program
1426            .predicates
1427            .iter()
1428            .rev()
1429            .find(|decl| arity_qualified_name(&decl.name, decl.arity()) == relation)
1430            .or_else(|| {
1431                presentation_program
1432                    .predicates
1433                    .iter()
1434                    .rev()
1435                    .find(|decl| decl.name == relation)
1436            });
1437        let source_columns = source_declaration.map(|declaration| declaration.schema_columns());
1438
1439        Some(
1440            schema
1441                .columns
1442                .iter()
1443                .enumerate()
1444                .map(|(index, (name, scalar_type))| {
1445                    let source_column = source_columns
1446                        .as_ref()
1447                        .and_then(|columns| columns.get(index));
1448                    LogicArgumentSchema {
1449                        name: name.clone(),
1450                        source_named: source_column
1451                            .and_then(|column| column.name.as_ref())
1452                            .is_some(),
1453                        sort: source_column.and_then(|column| match &column.typ {
1454                            TypeRef::Domain(name) => Some(name.clone()),
1455                            _ => None,
1456                        }),
1457                        scalar_type: *scalar_type,
1458                    }
1459                })
1460                .collect(),
1461        )
1462    }
1463
1464    /// Return stable rule provenance for source-visible rules.
1465    pub fn rule_provenance(&self) -> Vec<xlog_logic::RuleProvenance> {
1466        xlog_logic::source_diagnostics(&self.source_program, &self.program, None).0
1467    }
1468
1469    /// Return direct proof traces for source queries.
1470    pub fn proof_traces(&self) -> Vec<xlog_logic::QueryProofTrace> {
1471        xlog_logic::source_diagnostics(&self.source_program, &self.program, None).1
1472    }
1473
1474    fn presentation_program(&self) -> &Program {
1475        &self.source_program
1476    }
1477
1478    /// Create a persistent user-visible relation store initialized with inline facts.
1479    pub fn create_relation_store(
1480        &self,
1481        provider: Arc<CudaKernelProvider>,
1482    ) -> Result<RelationStore> {
1483        let mut store = RelationStore::new(provider.clone());
1484        for (name, schema) in &self.schemas {
1485            if is_user_visible_relation(name) || is_list_helper_relation(name) {
1486                store.put(name, provider.create_empty_buffer(schema.clone())?);
1487            }
1488        }
1489        self.load_facts_into_store(provider.as_ref(), &mut store)?;
1490        Ok(store)
1491    }
1492
1493    /// Evaluate using a persistent base relation store.
1494    ///
1495    /// The provided store is treated as immutable seed state. Buffers are cloned
1496    /// into a fresh executor for each evaluation so repeated evaluations reuse
1497    /// stored relations without mutating the persistent store itself.
1498    pub fn evaluate_with_relation_store(
1499        &self,
1500        provider: Arc<CudaKernelProvider>,
1501        relation_store: &RelationStore,
1502        profiling: bool,
1503    ) -> Result<LogicEvalResult> {
1504        let (result, _) =
1505            self.evaluate_with_relation_store_and_cache(provider, relation_store, profiling)?;
1506        Ok(result)
1507    }
1508
1509    /// Evaluate using a persistent relation store and return the complete runtime store.
1510    pub fn evaluate_with_relation_store_and_cache(
1511        &self,
1512        provider: Arc<CudaKernelProvider>,
1513        relation_store: &RelationStore,
1514        profiling: bool,
1515    ) -> Result<(LogicEvalResult, LogicMaterializedStore)> {
1516        self.reject_compiler_generated_query_relation_names(
1517            relation_store.names(),
1518            "persistent caller",
1519        )?;
1520        let resident_mode = ResidentSelectionMode::from_env()?;
1521        let mut executor =
1522            self.executor_from_materialized_store(provider.clone(), relation_store, profiling)?;
1523        executor.execute_plan(self.ordinary_plan("relation-store evaluation")?)?;
1524        self.enforce_constraints(&provider, &executor)?;
1525
1526        let total_output_rows = self.total_query_rows(executor.store())?;
1527        let mut stats = if profiling {
1528            Some(executor.execution_stats(total_output_rows))
1529        } else {
1530            None
1531        };
1532        if resident_mode.requested() {
1533            if resident_mode == ResidentSelectionMode::Require {
1534                return Err(XlogError::Execution(
1535                    "resident conditional-graph execution was required, but complete-store evaluation requires the existing GPU path"
1536                        .to_string(),
1537                ));
1538            }
1539            if let Some(stats) = stats.as_mut() {
1540                stats.resident_graph = Some(ResidentGraphExecutionStats::declined(
1541                    ResidentGraphDeclineReason::FullStoreRequested,
1542                ));
1543            }
1544        }
1545
1546        let cached_store = self.clone_relation_store(&provider, executor.store())?;
1547        let result = self.logic_result_from_store(provider.as_ref(), &cached_store, stats)?;
1548        Ok((result, self.bind_materialized_store(cached_store)))
1549    }
1550
1551    /// Create retained runtime state for a persistent relation session.
1552    pub fn create_session_runtime(
1553        &self,
1554        provider: Arc<CudaKernelProvider>,
1555        relation_store: &RelationStore,
1556        profiling: bool,
1557    ) -> Result<LogicSessionRuntime> {
1558        self.reject_compiler_generated_query_relation_names(
1559            relation_store.names(),
1560            "persistent caller",
1561        )?;
1562        self.ordinary_plan("persistent relation session")?;
1563        let executor =
1564            self.executor_from_materialized_store(provider, relation_store, profiling)?;
1565        Ok(LogicSessionRuntime {
1566            reusable_state_identity: self.reusable_state_identity.clone(),
1567            executor,
1568            profiling,
1569        })
1570    }
1571
1572    fn create_session_runtime_from_materialized_store(
1573        &self,
1574        provider: Arc<CudaKernelProvider>,
1575        relation_store: &LogicMaterializedStore,
1576        profiling: bool,
1577    ) -> Result<LogicSessionRuntime> {
1578        self.validate_reusable_state_identity(
1579            &relation_store.reusable_state_identity,
1580            "materialized cache",
1581        )?;
1582        self.ordinary_plan("materialized relation session")?;
1583        Ok(LogicSessionRuntime {
1584            reusable_state_identity: self.reusable_state_identity.clone(),
1585            executor: self.executor_from_materialized_store(
1586                provider,
1587                &relation_store.store,
1588                profiling,
1589            )?,
1590            profiling,
1591        })
1592    }
1593
1594    /// Evaluate with retained session runtime state and return a materialized store snapshot.
1595    pub fn evaluate_with_session_runtime(
1596        &self,
1597        provider: Arc<CudaKernelProvider>,
1598        runtime: &mut LogicSessionRuntime,
1599    ) -> Result<(LogicEvalResult, LogicMaterializedStore)> {
1600        self.validate_reusable_state_identity(&runtime.reusable_state_identity, "session runtime")?;
1601        let resident_mode = ResidentSelectionMode::from_env()?;
1602        runtime.executor.set_profiling(runtime.profiling);
1603        runtime
1604            .executor
1605            .execute_plan(self.ordinary_plan("session runtime evaluation")?)?;
1606        self.enforce_constraints(&provider, &runtime.executor)?;
1607
1608        let total_output_rows = self.total_query_rows(runtime.executor.store())?;
1609        let mut stats = if runtime.profiling {
1610            Some(runtime.executor.execution_stats(total_output_rows))
1611        } else {
1612            None
1613        };
1614        if resident_mode.requested() {
1615            if resident_mode == ResidentSelectionMode::Require {
1616                return Err(XlogError::Execution(
1617                    "resident conditional-graph execution was required, but persistent session evaluation requires the existing GPU path"
1618                        .to_string(),
1619                ));
1620            }
1621            if let Some(stats) = stats.as_mut() {
1622                stats.resident_graph = Some(ResidentGraphExecutionStats::declined(
1623                    ResidentGraphDeclineReason::FullStoreRequested,
1624                ));
1625            }
1626        }
1627
1628        let cached_store = self.clone_relation_store(&provider, runtime.executor.store())?;
1629        let result = self.logic_result_from_store(provider.as_ref(), &cached_store, stats)?;
1630        Ok((result, self.bind_materialized_store(cached_store)))
1631    }
1632
1633    /// Build query results from an already materialized runtime store.
1634    ///
1635    /// A raw relation store cannot attest that it was materialized by this
1636    /// compiled program:
1637    ///
1638    /// ```compile_fail
1639    /// use std::sync::Arc;
1640    /// use xlog_core::Result;
1641    /// use xlog_cuda::CudaKernelProvider;
1642    /// use xlog_gpu::logic::LogicProgram;
1643    /// use xlog_runtime::RelationStore;
1644    ///
1645    /// fn evaluate_untrusted_store(
1646    ///     program: &LogicProgram,
1647    ///     provider: Arc<CudaKernelProvider>,
1648    ///     raw_store: &RelationStore,
1649    /// ) -> Result<()> {
1650    ///     program.evaluate_cached_relation_store(provider, raw_store)?;
1651    ///     Ok(())
1652    /// }
1653    /// ```
1654    pub fn evaluate_cached_relation_store(
1655        &self,
1656        provider: Arc<CudaKernelProvider>,
1657        relation_store: &LogicMaterializedStore,
1658    ) -> Result<LogicEvalResult> {
1659        self.validate_reusable_state_identity(
1660            &relation_store.reusable_state_identity,
1661            "materialized cache",
1662        )?;
1663        self.logic_result_from_store(provider.as_ref(), &relation_store.store, None)
1664    }
1665
1666    /// Apply relation deltas to a persistent session store through the runtime delta path.
1667    ///
1668    /// A cache from another compiled program is rejected without consuming it.
1669    /// After identity validation, an operational preparation failure leaves the
1670    /// authoritative relation store unchanged but discards the consumed cache.
1671    pub fn apply_relation_deltas(
1672        &self,
1673        provider: Arc<CudaKernelProvider>,
1674        relation_store: &mut RelationStore,
1675        cached_store: &mut Option<LogicMaterializedStore>,
1676        deltas: HashMap<String, RelationDelta>,
1677    ) -> Result<LogicDeltaReport> {
1678        self.validate_reusable_state_slots(cached_store.as_ref(), None)?;
1679        let mut session_runtime = None;
1680        let prepared = self.prepare_relation_delta_commit(
1681            provider,
1682            relation_store,
1683            cached_store,
1684            &mut session_runtime,
1685            deltas,
1686            None,
1687        )?;
1688        Ok(prepared.commit())
1689    }
1690
1691    /// Apply relation deltas while preserving retained session runtime state.
1692    ///
1693    /// State from another compiled program is rejected without consuming it.
1694    /// After identity validation, an operational preparation failure leaves the
1695    /// authoritative store unchanged and the derived-state slots empty.
1696    pub fn apply_relation_deltas_with_session_runtime(
1697        &self,
1698        provider: Arc<CudaKernelProvider>,
1699        relation_store: &mut RelationStore,
1700        cached_store: &mut Option<LogicMaterializedStore>,
1701        session_runtime: &mut Option<LogicSessionRuntime>,
1702        deltas: HashMap<String, RelationDelta>,
1703    ) -> Result<LogicDeltaReport> {
1704        self.validate_reusable_state_slots(cached_store.as_ref(), session_runtime.as_ref())?;
1705        let prepared = self.prepare_relation_delta_commit(
1706            provider,
1707            relation_store,
1708            cached_store,
1709            session_runtime,
1710            deltas,
1711            None,
1712        )?;
1713        Ok(prepared.commit())
1714    }
1715
1716    /// Prepare raw relation deltas without ordered-batch coalescing.
1717    ///
1718    /// This preserves the runtime's delete-then-insert semantics when one
1719    /// relation delta contains both directions. Callers that accept an ordered
1720    /// batch must use [`LogicProgram::prepare_relation_delta_batch`] exactly
1721    /// once and pass its result to
1722    /// [`LogicProgram::prepare_relation_delta_commit_with_session_runtime`].
1723    /// Foreign reusable state is rejected before it is consumed. Once identity
1724    /// validation succeeds, the current cache and runtime are consumed during
1725    /// preparation; on operational error their caller slots remain empty while
1726    /// the authoritative store remains unchanged.
1727    pub fn prepare_relation_deltas_commit_with_session_runtime<'a>(
1728        &self,
1729        provider: Arc<CudaKernelProvider>,
1730        relation_store: &'a mut RelationStore,
1731        cached_store: &'a mut Option<LogicMaterializedStore>,
1732        session_runtime: &'a mut Option<LogicSessionRuntime>,
1733        deltas: HashMap<String, RelationDelta>,
1734    ) -> Result<PreparedRelationDeltaCommit<'a>> {
1735        self.validate_reusable_state_slots(cached_store.as_ref(), session_runtime.as_ref())?;
1736        self.prepare_relation_delta_commit(
1737            provider,
1738            relation_store,
1739            cached_store,
1740            session_runtime,
1741            deltas,
1742            None,
1743        )
1744    }
1745
1746    /// Coalesce an ordered batch on the device and retain cancellation tuples
1747    /// for selected relations.
1748    pub fn prepare_relation_delta_batch(
1749        &self,
1750        provider: &CudaKernelProvider,
1751        delta_batch: Vec<(String, RelationDelta)>,
1752        cancellation_capture_relations: &BTreeSet<String>,
1753    ) -> Result<PreparedRelationDeltaBatch> {
1754        self.reject_compiler_generated_query_relation_names(
1755            delta_batch.iter().map(|(name, _)| name.as_str()),
1756            "caller delta",
1757        )?;
1758        coalesce_relation_delta_batch_with_cancellation_capture(
1759            provider,
1760            delta_batch,
1761            cancellation_capture_relations,
1762        )
1763    }
1764
1765    /// Build the prospective authoritative base store for an independent
1766    /// full-recompute diagnostic without consuming retained session state.
1767    ///
1768    /// This must run before preparing the retained-runtime commit. If cloning
1769    /// or applying a base delta fails, the caller's cache and session runtime
1770    /// remain available for subsequent evaluations.
1771    pub fn clone_prospective_base_for_prepared_delta_batch(
1772        &self,
1773        provider: &Arc<CudaKernelProvider>,
1774        authoritative_relation_store: &RelationStore,
1775        prepared_batch: &PreparedRelationDeltaBatch,
1776    ) -> Result<RelationStore> {
1777        self.reject_compiler_generated_query_relation_names(
1778            authoritative_relation_store.names(),
1779            "persistent caller",
1780        )?;
1781
1782        let deltas = prepared_batch.net_deltas();
1783        let mut unchanged_names = authoritative_relation_store
1784            .names()
1785            .filter(|name| !deltas.contains_key(*name))
1786            .collect::<Vec<_>>();
1787        unchanged_names.sort_unstable();
1788
1789        let mut changed_names = deltas.keys().map(String::as_str).collect::<Vec<_>>();
1790        changed_names.sort_unstable();
1791
1792        let mut prospective = RelationStore::new(provider.clone());
1793        prospective.try_reserve_relations(unchanged_names.len() + changed_names.len())?;
1794
1795        for name in unchanged_names {
1796            let buffer = authoritative_relation_store.get(name).ok_or_else(|| {
1797                XlogError::Execution(format!(
1798                    "Authoritative relation {name} disappeared while cloning prospective base state"
1799                ))
1800            })?;
1801            let context = format!("cloning prospective base relation '{name}'");
1802            let cloned = provider
1803                .clone_buffer(buffer)
1804                .map_err(|error| relation_clone_error(context, error))?;
1805            prospective.put(name, cloned);
1806        }
1807
1808        for name in changed_names {
1809            let delta = deltas.get(name).ok_or_else(|| {
1810                XlogError::Execution(format!(
1811                    "Prepared relation delta for {name} disappeared while cloning prospective base state"
1812                ))
1813            })?;
1814            let existing = authoritative_relation_store.get(name);
1815            let schema = existing
1816                .map(|buffer| buffer.schema().clone())
1817                .or_else(|| delta.insert.as_ref().map(|buffer| buffer.schema().clone()))
1818                .or_else(|| delta.delete.as_ref().map(|buffer| buffer.schema().clone()))
1819                .ok_or_else(|| {
1820                    XlogError::Execution(format!(
1821                        "Delta update for {name} has no existing relation and no schema"
1822                    ))
1823                })?;
1824            let context = format!("cloning prospective base relation '{name}'");
1825            let mut updated = match existing {
1826                Some(buffer) => provider
1827                    .clone_buffer(buffer)
1828                    .map_err(|error| relation_clone_error(context, error))?,
1829                None => provider.create_empty_buffer(schema)?,
1830            };
1831            if let Some(delete) = &delta.delete {
1832                updated = provider.diff_gpu(&updated, delete)?;
1833            }
1834            if let Some(insert) = &delta.insert {
1835                updated = provider.union_gpu(&updated, insert)?;
1836            }
1837            prospective.put(name, updated);
1838        }
1839
1840        Ok(prospective)
1841    }
1842
1843    /// Prepare a fully staged retained-runtime commit from a coalesced batch.
1844    ///
1845    /// Foreign reusable state is rejected before the coalesced batch is consumed.
1846    /// Once identity validation succeeds, the current runtime and cache move into
1847    /// the transaction. An operational preparation failure discards those values
1848    /// and leaves the caller slots empty; the authoritative store is unchanged.
1849    pub fn prepare_relation_delta_commit_with_session_runtime<'a>(
1850        &self,
1851        provider: Arc<CudaKernelProvider>,
1852        relation_store: &'a mut RelationStore,
1853        cached_store: &'a mut Option<LogicMaterializedStore>,
1854        session_runtime: &'a mut Option<LogicSessionRuntime>,
1855        prepared_batch: PreparedRelationDeltaBatch,
1856    ) -> Result<PreparedRelationDeltaCommit<'a>> {
1857        self.validate_reusable_state_slots(cached_store.as_ref(), session_runtime.as_ref())?;
1858        let (deltas, report_seed) = prepared_batch.into_application_parts();
1859        self.prepare_relation_delta_commit(
1860            provider,
1861            relation_store,
1862            cached_store,
1863            session_runtime,
1864            deltas,
1865            Some(report_seed),
1866        )
1867    }
1868
1869    fn prepare_relation_delta_commit<'a>(
1870        &self,
1871        provider: Arc<CudaKernelProvider>,
1872        relation_store: &'a mut RelationStore,
1873        cached_store: &'a mut Option<LogicMaterializedStore>,
1874        session_runtime: &'a mut Option<LogicSessionRuntime>,
1875        deltas: HashMap<String, RelationDelta>,
1876        report_seed: Option<PreparedRelationDeltaReportSeed>,
1877    ) -> Result<PreparedRelationDeltaCommit<'a>> {
1878        self.validate_reusable_state_slots(cached_store.as_ref(), session_runtime.as_ref())?;
1879        self.reject_compiler_generated_query_relation_names(
1880            relation_store.names(),
1881            "persistent caller",
1882        )?;
1883        self.reject_compiler_generated_query_relation_names(
1884            deltas.keys().map(String::as_str),
1885            "caller delta",
1886        )?;
1887        let insert_rows = deltas
1888            .values()
1889            .filter_map(|delta| delta.insert.as_ref())
1890            .map(CudaBuffer::num_rows)
1891            .sum();
1892        let delete_rows = deltas
1893            .values()
1894            .filter_map(|delta| delta.delete.as_ref())
1895            .map(CudaBuffer::num_rows)
1896            .sum();
1897        let cache_reused = session_runtime.is_some() || cached_store.is_some();
1898        let mut changed_relation_names = deltas.keys().cloned().collect::<Vec<_>>();
1899        changed_relation_names.sort();
1900
1901        let prior_cached_store = cached_store.take();
1902        let prior_session_runtime = session_runtime.take();
1903
1904        let missing_relation_count = changed_relation_names
1905            .iter()
1906            .filter(|name| !relation_store.contains(name))
1907            .count();
1908        relation_store.try_reserve_relations(missing_relation_count)?;
1909
1910        if deltas.is_empty() {
1911            if let Some(seed) = report_seed {
1912                return Ok(PreparedRelationDeltaCommit {
1913                    provider,
1914                    authoritative_relation_store: relation_store,
1915                    cached_store_slot: cached_store,
1916                    session_runtime_slot: session_runtime,
1917                    staged_base_updates: Vec::new(),
1918                    prospective_cached_store: prior_cached_store,
1919                    prospective_session_runtime: prior_session_runtime,
1920                    report: no_op_delta_report(seed),
1921                });
1922            }
1923        }
1924
1925        let mut working_runtime = match prior_session_runtime {
1926            Some(runtime) => runtime,
1927            None => {
1928                if let Some(materialized_store) = prior_cached_store.as_ref() {
1929                    self.create_session_runtime_from_materialized_store(
1930                        provider.clone(),
1931                        materialized_store,
1932                        false,
1933                    )?
1934                } else {
1935                    self.create_session_runtime(provider.clone(), relation_store, false)?
1936                }
1937            }
1938        };
1939
1940        if prior_cached_store.is_none() {
1941            self.evaluate_with_session_runtime(provider.clone(), &mut working_runtime)?;
1942        }
1943
1944        let delta_stats = working_runtime.executor.apply_deltas_and_recompute(
1945            self.ordinary_plan("session relation-delta recompute")?,
1946            &deltas,
1947        )?;
1948        self.enforce_constraints(&provider, &working_runtime.executor)?;
1949
1950        let mut staged_base_updates = Vec::with_capacity(changed_relation_names.len());
1951        for name in &changed_relation_names {
1952            let updated = working_runtime.executor.store().get(name).ok_or_else(|| {
1953                XlogError::Execution(format!(
1954                    "Delta relation {} missing after runtime recompute",
1955                    name
1956                ))
1957            })?;
1958            let context = format!("cloning staged base relation '{name}'");
1959            staged_base_updates.push((
1960                name.clone(),
1961                provider
1962                    .clone_buffer(updated)
1963                    .map_err(|error| relation_clone_error(context, error))?,
1964            ));
1965        }
1966        let prospective_cached_store = Some(self.bind_materialized_store(
1967            self.clone_prepared_relation_snapshot(&provider, working_runtime.executor.store())?,
1968        ));
1969
1970        let mut report = logic_delta_report(delta_stats, insert_rows, delete_rows);
1971        report.changed_relation_names = changed_relation_names;
1972        report.planner_telemetry =
1973            DeltaPlannerTelemetry::from_delta_report(&report, cache_reused, None);
1974        report.debug_trace = delta_debug_trace(&report);
1975        if let Some(seed) = report_seed {
1976            report.input_delta_count = seed.input_delta_count;
1977            report.changed_relations = seed.changed_relations;
1978            report.coalesced_insert_rows = seed.coalesced_insert_rows;
1979            report.coalesced_delete_rows = seed.coalesced_delete_rows;
1980            report.canceled_rows = seed.canceled_rows;
1981            report.planner_telemetry =
1982                DeltaPlannerTelemetry::from_delta_report(&report, true, None);
1983            report.debug_trace = delta_debug_trace(&report);
1984        }
1985
1986        Ok(PreparedRelationDeltaCommit {
1987            provider,
1988            authoritative_relation_store: relation_store,
1989            cached_store_slot: cached_store,
1990            session_runtime_slot: session_runtime,
1991            staged_base_updates,
1992            prospective_cached_store,
1993            prospective_session_runtime: Some(working_runtime),
1994            report,
1995        })
1996    }
1997
1998    /// Apply an ordered batch of relation deltas after device-side coalescing.
1999    ///
2000    /// A fully canceled batch returns a no-op report without changing the
2001    /// authoritative store or advancing derived runtime state. If preparation
2002    /// fails, the authoritative store remains unchanged. A foreign cache is
2003    /// rejected before device coalescing and is not consumed; after successful
2004    /// identity validation an operational failure may discard the cache.
2005    pub fn apply_relation_delta_batch(
2006        &self,
2007        provider: Arc<CudaKernelProvider>,
2008        relation_store: &mut RelationStore,
2009        cached_store: &mut Option<LogicMaterializedStore>,
2010        delta_batch: Vec<(String, RelationDelta)>,
2011    ) -> Result<LogicDeltaReport> {
2012        self.validate_reusable_state_slots(cached_store.as_ref(), None)?;
2013        self.reject_compiler_generated_query_relation_names(
2014            relation_store.names(),
2015            "persistent caller",
2016        )?;
2017        let prepared_batch =
2018            self.prepare_relation_delta_batch(provider.as_ref(), delta_batch, &BTreeSet::new())?;
2019        let mut session_runtime = None;
2020        let prepared = self.prepare_relation_delta_commit_with_session_runtime(
2021            provider,
2022            relation_store,
2023            cached_store,
2024            &mut session_runtime,
2025            prepared_batch,
2026        )?;
2027        Ok(prepared.commit())
2028    }
2029
2030    /// Apply an ordered batch of relation deltas while preserving session runtime state.
2031    ///
2032    /// A fully canceled batch returns a no-op report without changing the
2033    /// authoritative store or advancing derived runtime state. If preparation
2034    /// fails, the authoritative store remains unchanged while the derived cache
2035    /// and retained runtime slots are left empty. Foreign state is rejected
2036    /// before device coalescing and remains in the caller slots.
2037    pub fn apply_relation_delta_batch_with_session_runtime(
2038        &self,
2039        provider: Arc<CudaKernelProvider>,
2040        relation_store: &mut RelationStore,
2041        cached_store: &mut Option<LogicMaterializedStore>,
2042        session_runtime: &mut Option<LogicSessionRuntime>,
2043        delta_batch: Vec<(String, RelationDelta)>,
2044    ) -> Result<LogicDeltaReport> {
2045        self.validate_reusable_state_slots(cached_store.as_ref(), session_runtime.as_ref())?;
2046        self.reject_compiler_generated_query_relation_names(
2047            relation_store.names(),
2048            "persistent caller",
2049        )?;
2050        let prepared_batch =
2051            self.prepare_relation_delta_batch(provider.as_ref(), delta_batch, &BTreeSet::new())?;
2052        let prepared = self.prepare_relation_delta_commit_with_session_runtime(
2053            provider,
2054            relation_store,
2055            cached_store,
2056            session_runtime,
2057            prepared_batch,
2058        )?;
2059        Ok(prepared.commit())
2060    }
2061
2062    /// Evaluate the program with the given input relations (no profiling).
2063    pub fn evaluate(
2064        &self,
2065        provider: Arc<CudaKernelProvider>,
2066        inputs: HashMap<String, CudaBuffer>,
2067    ) -> Result<LogicEvalResult> {
2068        self.evaluate_with_options(provider, inputs, false)
2069    }
2070
2071    fn finish_nonordinary_resident_selection(
2072        &self,
2073        mut result: LogicEvalResult,
2074        mode: ResidentSelectionMode,
2075    ) -> Result<LogicEvalResult> {
2076        match mode {
2077            ResidentSelectionMode::Disabled => Ok(result),
2078            ResidentSelectionMode::Prefer => {
2079                if let Some(stats) = result.stats.as_mut() {
2080                    stats.resident_graph = Some(ResidentGraphExecutionStats::declined(
2081                        ResidentGraphDeclineReason::NonOrdinaryPlan,
2082                    ));
2083                }
2084                Ok(result)
2085            }
2086            ResidentSelectionMode::Require => Err(XlogError::Execution(
2087                "resident conditional-graph execution was required for a non-ordinary program"
2088                    .to_string(),
2089            )),
2090        }
2091    }
2092
2093    fn compiler_generated_query_heads(&self) -> Result<BTreeSet<String>> {
2094        match &self.plan {
2095            LogicExecutionPlan::Ordinary(plan) => {
2096                if plan.generated_query_rules.len() != self.program.queries.len() {
2097                    return Err(XlogError::Execution(format!(
2098                            "compiler-generated query provenance count {} does not match authored query count {}",
2099                            plan.generated_query_rules.len(),
2100                            self.program.queries.len()
2101                        )));
2102                }
2103                let mut heads = BTreeSet::new();
2104                let mut rule_positions = BTreeSet::new();
2105                for (position, provenance) in plan.generated_query_rules.iter().enumerate() {
2106                    if provenance.query_index != position {
2107                        return Err(XlogError::Execution(format!(
2108                                "compiler-generated query provenance position {position} carries query index {}",
2109                                provenance.query_index
2110                            )));
2111                    }
2112                    if !rule_positions.insert((provenance.scc_index, provenance.rule_index)) {
2113                        return Err(XlogError::Execution(format!(
2114                                "compiler-generated query provenance {} reuses compiled rule scc={} rule={}",
2115                                provenance.query_index, provenance.scc_index, provenance.rule_index
2116                            )));
2117                    }
2118                    let expected_head = format!("__xlog_query_{}", provenance.query_index);
2119                    let rule = plan
2120                            .rules_by_scc
2121                            .get(provenance.scc_index)
2122                            .and_then(|rules| rules.get(provenance.rule_index))
2123                            .ok_or_else(|| {
2124                                XlogError::Execution(format!(
2125                                    "compiler-generated query provenance {} references missing compiled rule scc={} rule={}",
2126                                    provenance.query_index,
2127                                    provenance.scc_index,
2128                                    provenance.rule_index
2129                                ))
2130                            })?;
2131                    if rule.head != expected_head {
2132                        return Err(XlogError::Execution(format!(
2133                                "compiler-generated query provenance {} expects head {expected_head} but references authored head {}",
2134                                provenance.query_index, rule.head
2135                            )));
2136                    }
2137                    let occurrence_count = plan
2138                        .rules_by_scc
2139                        .iter()
2140                        .flatten()
2141                        .filter(|candidate| candidate.head == expected_head)
2142                        .count();
2143                    if occurrence_count != 1 {
2144                        return Err(XlogError::Execution(format!(
2145                                "compiler-generated query head {expected_head} must have exactly one compiled rule, found {occurrence_count}"
2146                            )));
2147                    }
2148                    heads.insert(expected_head);
2149                }
2150                Ok(heads)
2151            }
2152            _ => Ok((0..self.program.queries.len())
2153                .map(|index| format!("__xlog_query_{index}"))
2154                .collect()),
2155        }
2156    }
2157
2158    fn reject_compiler_generated_query_relation_names<'a>(
2159        &self,
2160        names: impl IntoIterator<Item = &'a str>,
2161        relation_source: &str,
2162    ) -> Result<()> {
2163        let generated_query_heads = self.compiler_generated_query_heads()?;
2164        if let Some(name) = names
2165            .into_iter()
2166            .find(|name| generated_query_heads.contains(*name))
2167        {
2168            return Err(XlogError::Execution(format!(
2169                "{relation_source} relation {name} collides with generated query head"
2170            )));
2171        }
2172        Ok(())
2173    }
2174
2175    fn evaluate_ordinary_with_resident_mode(
2176        &self,
2177        provider: Arc<CudaKernelProvider>,
2178        inputs: HashMap<String, CudaBuffer>,
2179        profiling: bool,
2180        mode: ResidentSelectionMode,
2181    ) -> Result<LogicEvalResult> {
2182        let mut latency_diagnostic =
2183            resident_latency_diagnostics_enabled().then(ResidentLatencyDiagnostic::new);
2184        let total_started = latency_diagnostic
2185            .as_ref()
2186            .map(|_| std::time::Instant::now());
2187        let certificate_input_started = latency_diagnostic
2188            .as_ref()
2189            .map(|_| std::time::Instant::now());
2190        let ordinary_plan = self.ordinary_plan("resident route certification")?;
2191        if self.program.queries.is_empty() {
2192            return self.evaluate_existing_gpu_after_resident_decline(
2193                provider,
2194                inputs,
2195                profiling,
2196                ordinary_plan,
2197                mode,
2198                ResidentGraphDeclineReason::FullStoreRequested,
2199            );
2200        }
2201        let certificate_initialization_started = latency_diagnostic
2202            .as_ref()
2203            .map(|_| std::time::Instant::now());
2204        let certification = if latency_diagnostic.is_some() {
2205            self.resident_certified_plan_with_outcome_for_plan(ordinary_plan)
2206        } else {
2207            self.resident_certified_plan_for_plan(ordinary_plan)
2208                .map(|certified| (certified, false, false))
2209        };
2210        let (certified_plan, certificate_cache_was_warm, certificate_initialized_here) =
2211            match certification {
2212                Ok(outcome) => outcome,
2213                Err(error) => {
2214                    return self.evaluate_existing_gpu_after_resident_decline(
2215                        provider,
2216                        inputs,
2217                        profiling,
2218                        ordinary_plan,
2219                        mode,
2220                        ResidentGraphDeclineReason::WorkspaceUnbounded {
2221                            detail: error.to_string(),
2222                        },
2223                    )
2224                }
2225            };
2226        let certificate = certified_plan.certificate();
2227        if !certificate.is_supported() {
2228            let reason = certificate.declines().first().cloned().unwrap_or_else(|| {
2229                ResidentGraphDeclineReason::WorkspaceUnbounded {
2230                    detail: "route inspection did not produce a resident certificate".into(),
2231                }
2232            });
2233            return self.evaluate_existing_gpu_after_resident_decline(
2234                provider,
2235                inputs,
2236                profiling,
2237                ordinary_plan,
2238                mode,
2239                reason,
2240            );
2241        }
2242        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2243            let elapsed_ns = resident_latency_elapsed_ns(certificate_initialization_started);
2244            diagnostic.certificate_cache_was_warm = certificate_cache_was_warm;
2245            diagnostic.certificate_initialized_here = certificate_initialized_here;
2246            if certificate_initialized_here {
2247                diagnostic.certificate_initialization_ns = elapsed_ns;
2248            } else {
2249                diagnostic.certificate_cache_access_ns = elapsed_ns;
2250            }
2251        }
2252        let input_setup_started = latency_diagnostic
2253            .as_ref()
2254            .map(|_| std::time::Instant::now());
2255
2256        for (name, buffer) in &inputs {
2257            let expected_schema = self.schemas.get(name).ok_or_else(|| {
2258                XlogError::Execution(format!(
2259                    "Input relation {name} not declared in program schemas"
2260                ))
2261            })?;
2262            ensure_schema_type_compatible(expected_schema, buffer.schema()).map_err(|error| {
2263                XlogError::Execution(format!("Input relation {name} schema mismatch: {error}"))
2264            })?;
2265        }
2266
2267        if let Some(relation) = inputs.iter().find_map(|(name, buffer)| {
2268            (!Self::resident_input_is_local(&provider, buffer)).then(|| name.clone())
2269        }) {
2270            return self.evaluate_existing_gpu_after_resident_decline(
2271                provider,
2272                inputs,
2273                profiling,
2274                ordinary_plan,
2275                mode,
2276                ResidentGraphDeclineReason::ImportedInputUnsupported { relation },
2277            );
2278        }
2279
2280        for (name, buffer) in &inputs {
2281            if !buffer.canonical_full_row_set_certified() {
2282                provider
2283                    .validated_logical_row_count(buffer)
2284                    .map_err(|error| {
2285                        XlogError::Execution(format!(
2286                            "Input relation {name} has invalid logical row metadata: {error}"
2287                        ))
2288                    })?;
2289            }
2290        }
2291
2292        let caller_has_runtime = provider.memory().runtime().is_some();
2293        let resident_provider = match Self::resident_provider_view(&provider) {
2294            Ok(provider) => provider,
2295            Err(reason) => {
2296                return self.evaluate_existing_gpu_after_resident_decline(
2297                    provider,
2298                    inputs,
2299                    profiling,
2300                    ordinary_plan,
2301                    mode,
2302                    reason,
2303                )
2304            }
2305        };
2306        let runtime = Arc::clone(
2307            resident_provider
2308                .memory()
2309                .runtime()
2310                .expect("resident provider view was validated with a runtime"),
2311        );
2312        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2313            diagnostic.runtime_bytes[0] = runtime.bytes_outstanding();
2314            diagnostic.manager_bytes[0] = resident_provider.memory().allocated_bytes();
2315        }
2316
2317        let resident_inputs = if caller_has_runtime {
2318            inputs
2319        } else {
2320            let mut migrated = HashMap::with_capacity(inputs.len());
2321            let migration = inputs.iter().try_for_each(|(name, buffer)| {
2322                resident_provider
2323                    .clone_buffer(buffer)
2324                    .map(|clone| migrated.insert(name.clone(), clone))
2325                    .map(|_| ())
2326            });
2327            if let Err(error) = migration {
2328                drop(migrated);
2329                resident_provider.device().synchronize().map_err(|cleanup| {
2330                        XlogError::Kernel(format!(
2331                            "resident input migration failed ({error}); cleanup synchronization failed: {cleanup}"
2332                        ))
2333                    })?;
2334                runtime.reap_pending().map_err(|cleanup| {
2335                    XlogError::Kernel(format!(
2336                        "resident input migration failed ({error}); cleanup reap failed: {cleanup}"
2337                    ))
2338                })?;
2339                return self.evaluate_existing_gpu_after_resident_decline(
2340                    provider,
2341                    inputs,
2342                    profiling,
2343                    ordinary_plan,
2344                    mode,
2345                    ResidentGraphDeclineReason::WorkspaceUnbounded {
2346                        detail: format!("input migration to the async runtime failed: {error}"),
2347                    },
2348                );
2349            }
2350            resident_provider.device().synchronize().map_err(|error| {
2351                XlogError::Kernel(format!(
2352                    "resident input migration synchronization failed: {error}"
2353                ))
2354            })?;
2355            drop(inputs);
2356            migrated
2357        };
2358
2359        let mut canonical_replacements = HashMap::new();
2360        let canonicalization = resident_inputs.iter().try_for_each(|(name, buffer)| {
2361            let expected_schema = self.schemas.get(name).ok_or_else(|| {
2362                XlogError::Execution(format!(
2363                    "Input relation {name} not declared in program schemas"
2364                ))
2365            })?;
2366            ensure_schema_type_compatible(expected_schema, buffer.schema()).map_err(|error| {
2367                XlogError::Execution(format!("Input relation {name} schema mismatch: {error}"))
2368            })?;
2369            if buffer.schema() == expected_schema && buffer.canonical_full_row_set_certified() {
2370                return Ok(());
2371            }
2372            let mut normalized = None;
2373            let canonical_source = if buffer.schema() == expected_schema {
2374                buffer
2375            } else {
2376                let mut clone = resident_provider.clone_buffer(buffer)?;
2377                clone.set_schema(expected_schema.clone());
2378                normalized.insert(clone)
2379            };
2380            let canonical = resident_provider.union_many_gpu(&[canonical_source])?;
2381            if !canonical.canonical_full_row_set_certified() {
2382                return Err(XlogError::Execution(format!(
2383                    "resident input {name} did not acquire a full-row set proof"
2384                )));
2385            }
2386            canonical_replacements.insert(name.clone(), canonical);
2387            Ok(())
2388        });
2389        if let Err(error) = canonicalization {
2390            drop(canonical_replacements);
2391            resident_provider.device().synchronize().map_err(|cleanup| {
2392                    XlogError::Kernel(format!(
2393                        "resident input canonicalization failed ({error}); cleanup synchronization failed: {cleanup}"
2394                    ))
2395                })?;
2396            runtime.reap_pending().map_err(|cleanup| {
2397                    XlogError::Kernel(format!(
2398                        "resident input canonicalization failed ({error}); cleanup reap failed: {cleanup}"
2399                    ))
2400                })?;
2401            return self.evaluate_existing_gpu_after_resident_decline(
2402                resident_provider,
2403                resident_inputs,
2404                profiling,
2405                ordinary_plan,
2406                mode,
2407                ResidentGraphDeclineReason::WorkspaceUnbounded {
2408                    detail: format!("input full-row canonicalization failed: {error}"),
2409                },
2410            );
2411        }
2412        let resident_inputs = resident_inputs
2413            .into_iter()
2414            .map(|(name, buffer)| {
2415                let canonical = canonical_replacements.remove(&name).unwrap_or(buffer);
2416                (name, canonical)
2417            })
2418            .collect();
2419
2420        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2421            diagnostic.input_setup_ns = resident_latency_elapsed_ns(input_setup_started);
2422            diagnostic.certificate_input_ns =
2423                resident_latency_elapsed_ns(certificate_input_started);
2424            diagnostic.runtime_bytes[1] = runtime.bytes_outstanding();
2425            diagnostic.manager_bytes[1] = resident_provider.memory().allocated_bytes();
2426        }
2427        let prepare_started = latency_diagnostic
2428            .as_ref()
2429            .map(|_| std::time::Instant::now());
2430        let mut executor = self.prepare_resident_executor(
2431            &resident_provider,
2432            resident_inputs,
2433            profiling,
2434            ordinary_plan,
2435        )?;
2436        let prepare_options = latency_diagnostic
2437            .as_ref()
2438            .map(|diagnostic| {
2439                ResidentGraphPrepareOptions::default()
2440                    .with_latency_diagnostic_sample(diagnostic.sample)
2441            })
2442            .unwrap_or_default();
2443        let mut prepared = match executor
2444            .prepare_certified_resident_graph(certified_plan.as_ref(), prepare_options)
2445        {
2446            Ok(prepared) => prepared,
2447            Err(ResidentGraphExecutionError::Declined(reason)) => {
2448                runtime
2449                    .reap_pending()
2450                    .map_err(|error| XlogError::Kernel(error.to_string()))?;
2451                return match mode {
2452                        ResidentSelectionMode::Prefer => {
2453                            executor.execute_plan(ordinary_plan)?;
2454                            let mut result = self.finish_ordinary_evaluation(
2455                                &resident_provider,
2456                                executor,
2457                                profiling,
2458                                None,
2459                                None,
2460                            )?;
2461                            if let Some(stats) = result.stats.as_mut() {
2462                                stats.resident_graph =
2463                                    Some(ResidentGraphExecutionStats::declined(reason));
2464                            }
2465                            Ok(result)
2466                        }
2467                        ResidentSelectionMode::Require => Err(XlogError::Execution(format!(
2468                            "resident conditional-graph execution was required but declined: {reason:?}"
2469                        ))),
2470                        ResidentSelectionMode::Disabled => unreachable!(
2471                            "disabled resident selection does not call the resident evaluator"
2472                        ),
2473                    };
2474            }
2475            Err(error) => return Err(Self::resident_execution_error(error)),
2476        };
2477        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2478            diagnostic.prepare_capture_allocation_ns = resident_latency_elapsed_ns(prepare_started);
2479            diagnostic.runtime_bytes[2] = runtime.bytes_outstanding();
2480            diagnostic.manager_bytes[2] = resident_provider.memory().allocated_bytes();
2481        }
2482        let prepare_diagnostic = prepared.take_prepare_diagnostic();
2483
2484        let transfer_before = resident_provider.host_transfer_stats();
2485        let provider_dtoh_before = resident_provider.d2h_transfer_count();
2486        let untracked_dtoh_before = resident_provider.untracked_metadata_dtoh_count();
2487        let deterministic_d2h_before = resident_provider.deterministic_d2h_violation_count();
2488        let final_before = resident_provider.final_observation_transfer_stats();
2489        let graph_before = runtime.conditional_graph_stats();
2490
2491        let launch_started = latency_diagnostic
2492            .as_ref()
2493            .map(|_| std::time::Instant::now());
2494        let in_flight = prepared.launch().map_err(Self::resident_execution_error)?;
2495        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2496            diagnostic.launch_submission_ns = resident_latency_elapsed_ns(launch_started);
2497            diagnostic.runtime_bytes[3] = runtime.bytes_outstanding();
2498            diagnostic.manager_bytes[3] = resident_provider.memory().allocated_bytes();
2499        }
2500        let sync_started = latency_diagnostic
2501            .as_ref()
2502            .map(|_| std::time::Instant::now());
2503        let synchronized = in_flight
2504            .synchronize_core()
2505            .map_err(Self::resident_execution_error)?;
2506        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2507            diagnostic.sync_wall_ns = resident_latency_elapsed_ns(sync_started);
2508            diagnostic.runtime_bytes[4] = runtime.bytes_outstanding();
2509            diagnostic.manager_bytes[4] = resident_provider.memory().allocated_bytes();
2510        }
2511
2512        let transfer_after = resident_provider.host_transfer_stats();
2513        let provider_dtoh_after = resident_provider.d2h_transfer_count();
2514        let untracked_dtoh_after = resident_provider.untracked_metadata_dtoh_count();
2515        let deterministic_d2h_after = resident_provider.deterministic_d2h_violation_count();
2516        let final_before_observation = resident_provider.final_observation_transfer_stats();
2517        let graph_after = runtime.conditional_graph_stats();
2518        let core_transfers = ResidentGraphCoreTransferStats {
2519            tracked_htod_calls: transfer_after
2520                .htod_calls
2521                .saturating_sub(transfer_before.htod_calls),
2522            tracked_htod_bytes: transfer_after
2523                .htod_bytes
2524                .saturating_sub(transfer_before.htod_bytes),
2525            tracked_dtoh_calls: transfer_after
2526                .dtoh_calls
2527                .saturating_sub(transfer_before.dtoh_calls),
2528            tracked_dtoh_bytes: transfer_after
2529                .dtoh_bytes
2530                .saturating_sub(transfer_before.dtoh_bytes),
2531            provider_dtoh_calls: provider_dtoh_after.saturating_sub(provider_dtoh_before),
2532            untracked_metadata_dtoh_calls: untracked_dtoh_after
2533                .saturating_sub(untracked_dtoh_before),
2534        };
2535        if core_transfers.tracked_htod_calls != 0
2536            || core_transfers.tracked_htod_bytes != 0
2537            || core_transfers.tracked_dtoh_calls != 0
2538            || core_transfers.tracked_dtoh_bytes != 0
2539            || core_transfers.provider_dtoh_calls != 0
2540            || core_transfers.untracked_metadata_dtoh_calls != 0
2541            || final_before_observation.dtoh_calls != final_before.dtoh_calls
2542            || final_before_observation.dtoh_bytes != final_before.dtoh_bytes
2543            || final_before_observation.pinned_receipts != final_before.pinned_receipts
2544        {
2545            return Err(XlogError::Execution(
2546                "resident conditional-graph core performed a host transfer".into(),
2547            ));
2548        }
2549        let graph_launches = graph_after.launches.saturating_sub(graph_before.launches);
2550        let terminal_synchronizations = graph_after
2551            .terminal_synchronizations
2552            .saturating_sub(graph_before.terminal_synchronizations);
2553        let host_iterations = graph_after
2554            .host_iterations
2555            .saturating_sub(graph_before.host_iterations);
2556        let host_allocations = graph_after
2557            .host_allocations
2558            .saturating_sub(graph_before.host_allocations);
2559        let host_status_injections = graph_after
2560            .host_status_injections
2561            .saturating_sub(graph_before.host_status_injections);
2562        let deterministic_d2h_violations =
2563            deterministic_d2h_after.saturating_sub(deterministic_d2h_before);
2564        if graph_launches != 1
2565            || terminal_synchronizations != 1
2566            || host_iterations != 0
2567            || host_allocations != 0
2568            || host_status_injections != 0
2569            || deterministic_d2h_violations != 0
2570        {
2571            return Err(XlogError::Execution(format!(
2572                    "resident conditional-graph runtime invariant failed: launches={graph_launches}, terminal_synchronizations={terminal_synchronizations}, host_iterations={host_iterations}, host_allocations={host_allocations}, host_status_injections={host_status_injections}, deterministic_d2h_violations={deterministic_d2h_violations}"
2573                )));
2574        }
2575
2576        let observation_started = latency_diagnostic
2577            .as_ref()
2578            .map(|_| std::time::Instant::now());
2579        let observed = synchronized
2580            .observe_final_receipt()
2581            .map_err(Self::resident_execution_error)?;
2582        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2583            let observation_ns = resident_latency_elapsed_ns(observation_started);
2584            let phase = observed.phase_timings().ok_or_else(|| {
2585                XlogError::Execution(
2586                    "resident latency diagnostics missing final-observation timings".into(),
2587                )
2588            })?;
2589            diagnostic.receipt_d2h_ns = phase.receipt_d2h_ns;
2590            diagnostic.receipt_decode_schema_staging_ns = phase.decode_schema_staging_ns;
2591            diagnostic.owner_teardown_residual_ns = observation_ns
2592                .saturating_sub(phase.receipt_d2h_ns)
2593                .saturating_sub(phase.decode_schema_staging_ns);
2594            diagnostic.staged_outputs = observed.staged_output_count();
2595            diagnostic.relation_registrations = observed.relation_registration_count();
2596            diagnostic.runtime_bytes[5] = runtime.bytes_outstanding();
2597            diagnostic.manager_bytes[5] = resident_provider.memory().allocated_bytes();
2598        }
2599        let encoded_len = u64::try_from(observed.encoded_len())
2600            .map_err(|_| XlogError::Execution("resident receipt byte length exceeds u64".into()))?;
2601        let device_elapsed_ns = observed.device_elapsed_ns();
2602        let device_scan_invocations = observed.device_scan_invocations();
2603        let device_filter_invocations = observed.device_filter_invocations();
2604        let semantic_scan_invocations = observed.semantic_scan_invocations();
2605        let semantic_filter_invocations = observed.semantic_filter_invocations();
2606        let staged_store_mutations = observed.staged_output_count();
2607        let iterations = observed.iterations();
2608        let final_after = resident_provider.final_observation_transfer_stats();
2609        let final_observation = ResidentGraphFinalObservationStats {
2610            dtoh_calls: final_after
2611                .dtoh_calls
2612                .saturating_sub(final_before_observation.dtoh_calls),
2613            dtoh_bytes: final_after
2614                .dtoh_bytes
2615                .saturating_sub(final_before_observation.dtoh_bytes),
2616            pinned_receipts: final_after
2617                .pinned_receipts
2618                .saturating_sub(final_before_observation.pinned_receipts),
2619        };
2620        if final_observation.dtoh_calls != 1
2621            || final_observation.dtoh_bytes != encoded_len
2622            || final_observation.pinned_receipts != 1
2623        {
2624            return Err(XlogError::Execution(format!(
2625                    "resident final observation invariant failed: calls={}, bytes={}, pinned={} expected_bytes={encoded_len}",
2626                    final_observation.dtoh_calls,
2627                    final_observation.dtoh_bytes,
2628                    final_observation.pinned_receipts,
2629                )));
2630        }
2631        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2632            diagnostic.device_event_ns = device_elapsed_ns;
2633        }
2634        let commit_started = latency_diagnostic
2635            .as_ref()
2636            .map(|_| std::time::Instant::now());
2637        observed
2638            .commit(&mut executor)
2639            .map_err(Self::resident_execution_error)?;
2640        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2641            diagnostic.commit_ns = resident_latency_elapsed_ns(commit_started);
2642            diagnostic.runtime_bytes[6] = runtime.bytes_outstanding();
2643            diagnostic.manager_bytes[6] = resident_provider.memory().allocated_bytes();
2644        }
2645
2646        let telemetry_started = latency_diagnostic
2647            .as_ref()
2648            .map(|_| std::time::Instant::now());
2649        let timed_scan_filter_invocations = device_scan_invocations
2650            .checked_add(device_filter_invocations)
2651            .ok_or_else(|| {
2652                XlogError::Execution("resident device invocation count overflow".into())
2653            })?;
2654        let telemetry = ResidentGraphExecutionStats {
2655            selection: ResidentGraphSelectionKind::ResidentConditionalGraph,
2656            decline: None,
2657            conditional_graph_launches: graph_launches,
2658            terminal_synchronizations,
2659            host_iterations,
2660            host_allocations,
2661            host_status_injections,
2662            deterministic_d2h_violations,
2663            host_dispatched_scan_ops: 0,
2664            host_dispatched_filter_ops: 0,
2665            device_scan_invocations,
2666            device_filter_invocations,
2667            semantic_scan_invocations,
2668            semantic_filter_invocations,
2669            staged_store_mutations,
2670            deferred_profile: ResidentGraphDeferredProfile {
2671                timed_scan_filter_invocations,
2672                device_elapsed_ns,
2673                final_sync_misattributed_ns: 0,
2674            },
2675            core_transfers,
2676            final_observation,
2677        };
2678        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2679            diagnostic.result_stats_construction_ns =
2680                resident_latency_elapsed_ns(telemetry_started);
2681        }
2682        let result = self.finish_ordinary_evaluation(
2683            &resident_provider,
2684            executor,
2685            profiling,
2686            Some(ResidentCompletedProfile {
2687                telemetry,
2688                iterations,
2689            }),
2690            latency_diagnostic.as_mut(),
2691        )?;
2692        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2693            diagnostic.runtime_bytes[7] = runtime.bytes_outstanding();
2694            diagnostic.manager_bytes[7] = resident_provider.memory().allocated_bytes();
2695        }
2696        let diagnostic_lines = if latency_diagnostic.is_some() {
2697            let total_ns = resident_latency_elapsed_ns(total_started);
2698            finalized_resident_latency_diagnostic_lines(
2699                total_ns,
2700                latency_diagnostic.as_ref(),
2701                prepare_diagnostic
2702                    .map(|diagnostic| move || diagnostic.into_snapshot().format_line()),
2703                ResidentLatencyDiagnostic::format_line,
2704            )
2705        } else {
2706            None
2707        };
2708        if let Some(diagnostic_lines) = diagnostic_lines {
2709            for line in diagnostic_lines.into_iter().flatten() {
2710                eprintln!("{line}");
2711            }
2712        }
2713        Ok(result)
2714    }
2715
2716    fn evaluate_existing_gpu_after_resident_decline(
2717        &self,
2718        provider: Arc<CudaKernelProvider>,
2719        inputs: HashMap<String, CudaBuffer>,
2720        profiling: bool,
2721        plan: &ExecutionPlan,
2722        mode: ResidentSelectionMode,
2723        reason: ResidentGraphDeclineReason,
2724    ) -> Result<LogicEvalResult> {
2725        match mode {
2726            ResidentSelectionMode::Require => Err(XlogError::Execution(format!(
2727                "resident conditional-graph execution was required but declined: {reason:?}"
2728            ))),
2729            ResidentSelectionMode::Prefer | ResidentSelectionMode::Disabled => {
2730                let mut executor = self.prepare_executor(&provider, inputs, profiling)?;
2731                executor.execute_plan(plan)?;
2732                let mut result =
2733                    self.finish_ordinary_evaluation(&provider, executor, profiling, None, None)?;
2734                if let Some(stats) = result.stats.as_mut() {
2735                    stats.resident_graph = Some(ResidentGraphExecutionStats::declined(reason));
2736                }
2737                Ok(result)
2738            }
2739        }
2740    }
2741
2742    fn resident_input_is_local(provider: &CudaKernelProvider, buffer: &CudaBuffer) -> bool {
2743        let expected_manager = Arc::as_ptr(provider.memory()) as usize;
2744        buffer.num_rows_device().memory_manager_ptr_value() == expected_manager
2745            && buffer.columns().iter().all(|column| {
2746                matches!(
2747                    column,
2748                    CudaColumn::Owned(slice)
2749                        if slice.memory_manager_ptr_value() == expected_manager
2750                )
2751            })
2752    }
2753
2754    fn resident_provider_view(
2755        provider: &Arc<CudaKernelProvider>,
2756    ) -> std::result::Result<Arc<CudaKernelProvider>, ResidentGraphDeclineReason> {
2757        let decline =
2758            |detail: String| ResidentGraphDeclineReason::ConditionalGraphUnavailable { detail };
2759        if !Arc::ptr_eq(provider.device(), provider.memory().device()) {
2760            return Err(decline(
2761                "provider and memory manager do not share the same CUDA device handle".into(),
2762            ));
2763        }
2764        if let Some(runtime) = provider.memory().runtime() {
2765            if !Arc::ptr_eq(provider.device(), runtime.device())
2766                || !runtime.supports_block_use_tracking()
2767            {
2768                return Err(decline(
2769                    "the caller runtime cannot track resident cross-stream block uses".into(),
2770                ));
2771            }
2772            return Ok(Arc::clone(provider));
2773        }
2774
2775        let device = Arc::clone(provider.device());
2776        let device_ordinal = u32::try_from(device.ordinal()).map_err(|_| {
2777            decline(format!(
2778                "CUDA device ordinal {} is not representable as u32",
2779                device.ordinal()
2780            ))
2781        })?;
2782        let budget_limit =
2783            usize::try_from(provider.memory().budget_limit_bytes()).map_err(|_| {
2784                decline("the caller memory budget is not representable as usize".into())
2785            })?;
2786        let stream_pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
2787        let asynchronous: Box<dyn DeviceMemoryResource + Send + Sync> =
2788            Box::new(AsyncCudaResource::new(
2789                Arc::clone(&device),
2790                device_ordinal,
2791                Arc::clone(&stream_pool),
2792            ));
2793        let resource: Box<dyn DeviceMemoryResource + Send + Sync> =
2794            Box::new(GlobalDeviceBudget::new(asynchronous, budget_limit));
2795        let runtime = Arc::new(XlogDeviceRuntime::with_resource(
2796            Arc::clone(&device),
2797            device_ordinal,
2798            stream_pool,
2799            resource,
2800        ));
2801        let overlay = provider
2802            .memory()
2803            .with_runtime_overlay(runtime)
2804            .map_err(|error| decline(error.to_string()))?;
2805        provider
2806            .with_runtime_memory_view(overlay)
2807            .map(Arc::new)
2808            .map_err(|error| decline(error.to_string()))
2809    }
2810
2811    fn resident_execution_error(error: ResidentGraphExecutionError) -> XlogError {
2812        XlogError::Execution(error.to_string())
2813    }
2814
2815    fn finish_ordinary_evaluation(
2816        &self,
2817        provider: &Arc<CudaKernelProvider>,
2818        mut executor: Executor,
2819        profiling: bool,
2820        resident_profile: Option<ResidentCompletedProfile>,
2821        mut latency_diagnostic: Option<&mut ResidentLatencyDiagnostic>,
2822    ) -> Result<LogicEvalResult> {
2823        let result_started = latency_diagnostic
2824            .as_ref()
2825            .map(|_| std::time::Instant::now());
2826        self.enforce_constraints(provider, &executor)?;
2827
2828        let mut queries = Vec::with_capacity(self.program.queries.len());
2829        for (index, query) in self.program.queries.iter().enumerate() {
2830            let internal_relation_name = format!("__xlog_query_{index}");
2831            let buffer = executor
2832                .store_mut()
2833                .remove(&internal_relation_name)
2834                .ok_or_else(|| {
2835                    XlogError::Execution(format!(
2836                        "Missing query result relation {internal_relation_name} (compiler bug?)"
2837                    ))
2838                })?;
2839            queries.push(self.logic_query_result(
2840                provider.as_ref(),
2841                index,
2842                query,
2843                internal_relation_name,
2844                buffer,
2845            )?);
2846        }
2847
2848        let total_output_rows = queries
2849            .iter()
2850            .map(|query| {
2851                query
2852                    .buffer
2853                    .cached_row_count()
2854                    .map(u64::from)
2855                    .unwrap_or_else(|| query.buffer.num_rows())
2856            })
2857            .sum();
2858        let mut stats = profiling.then(|| executor.execution_stats(total_output_rows));
2859        if let (Some(stats), Some(profile)) = (stats.as_mut(), resident_profile) {
2860            let scan_count =
2861                usize::try_from(profile.telemetry.device_scan_invocations).map_err(|_| {
2862                    XlogError::Execution("resident scan profile count exceeds usize".into())
2863                })?;
2864            let filter_count = usize::try_from(profile.telemetry.device_filter_invocations)
2865                .map_err(|_| {
2866                    XlogError::Execution("resident filter profile count exceeds usize".into())
2867                })?;
2868            let (num_rules, is_recursive) = match &self.plan {
2869                LogicExecutionPlan::Ordinary(plan) => (
2870                    plan.rules_by_scc.iter().map(Vec::len).sum(),
2871                    plan.sccs.iter().any(|scc| scc.is_recursive),
2872                ),
2873                _ => (0, false),
2874            };
2875            let mut stratum = StratumStats::new(0, num_rules, is_recursive);
2876            stratum.iterations = profile.iterations as usize;
2877            stratum.duration_us = profile.telemetry.deferred_profile.device_elapsed_ns / 1_000;
2878            stratum.ops.reserve(scan_count.saturating_add(filter_count));
2879            stratum.ops.extend((0..scan_count).map(|_| OpStats {
2880                op_name: "scan".to_string(),
2881                ..OpStats::default()
2882            }));
2883            stratum.ops.extend((0..filter_count).map(|_| OpStats {
2884                op_name: "filter".to_string(),
2885                ..OpStats::default()
2886            }));
2887            stats.total_duration_us = stratum.duration_us;
2888            stats.strata = vec![stratum];
2889            stats.resident_graph = Some(profile.telemetry);
2890        }
2891
2892        let result = LogicEvalResult { queries, stats };
2893        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2894            diagnostic.result_stats_construction_ns = diagnostic
2895                .result_stats_construction_ns
2896                .saturating_add(resident_latency_elapsed_ns(result_started));
2897            diagnostic.remaining_store_relations_before_drop = executor.store().len();
2898        }
2899        let executor_drop_started = latency_diagnostic
2900            .as_ref()
2901            .map(|_| std::time::Instant::now());
2902        drop(executor);
2903        if let Some(diagnostic) = latency_diagnostic.as_mut() {
2904            diagnostic.executor_store_teardown_ns =
2905                resident_latency_elapsed_ns(executor_drop_started);
2906        }
2907        Ok(result)
2908    }
2909
2910    fn prepare_resident_executor(
2911        &self,
2912        provider: &Arc<CudaKernelProvider>,
2913        inputs: HashMap<String, CudaBuffer>,
2914        profiling: bool,
2915        plan: &ExecutionPlan,
2916    ) -> Result<Executor> {
2917        let derived_relations = plan
2918            .rules_by_scc
2919            .iter()
2920            .flatten()
2921            .map(|rule| rule.head.clone())
2922            .collect::<BTreeSet<_>>();
2923        self.prepare_executor_excluding_derived_placeholders(
2924            provider,
2925            inputs,
2926            profiling,
2927            Some(&derived_relations),
2928        )
2929    }
2930
2931    fn prepare_executor_excluding_derived_placeholders(
2932        &self,
2933        provider: &Arc<CudaKernelProvider>,
2934        inputs: HashMap<String, CudaBuffer>,
2935        profiling: bool,
2936        derived_relations: Option<&BTreeSet<String>>,
2937    ) -> Result<Executor> {
2938        let mut executor = Executor::new(provider.clone());
2939        executor.set_profiling(profiling);
2940        for (name, rel_id) in &self.rel_ids {
2941            executor.register_relation(*rel_id, name);
2942        }
2943
2944        let arity_qualified_predicates = if self.epistemic_provenance.is_some() {
2945            epistemic_extensional_multi_arity_predicates(&self.program)
2946        } else {
2947            predicate_arities(&self.program)
2948                .into_iter()
2949                .filter_map(|(predicate, arities)| (arities.len() > 1).then_some(predicate))
2950                .collect()
2951        };
2952        let inline_fact_relations = self
2953            .program
2954            .facts()
2955            .map(|fact| {
2956                let predicate = fact.head.predicate.as_str();
2957                if arity_qualified_predicates.contains(predicate) {
2958                    arity_qualified_name(predicate, fact.head.terms.len())
2959                } else {
2960                    predicate.to_string()
2961                }
2962            })
2963            .collect::<BTreeSet<_>>();
2964
2965        for (name, schema) in &self.schemas {
2966            let is_derived_placeholder = derived_relations.is_some_and(|set| set.contains(name))
2967                && !inline_fact_relations.contains(name);
2968            if is_derived_placeholder {
2969                continue;
2970            }
2971            executor
2972                .store_mut()
2973                .put(name, provider.create_empty_buffer(schema.clone())?);
2974        }
2975
2976        for (name, buffer) in inputs {
2977            let schema = self.schemas.get(&name).ok_or_else(|| {
2978                XlogError::Execution(format!(
2979                    "Input relation {} not declared in program schemas",
2980                    name
2981                ))
2982            })?;
2983            ensure_schema_type_compatible(schema, buffer.schema()).map_err(|e| {
2984                XlogError::Execution(format!("Input relation {} schema mismatch: {}", name, e))
2985            })?;
2986            executor.store_mut().put(&name, buffer);
2987        }
2988
2989        self.load_facts(provider, &mut executor)?;
2990        Ok(executor)
2991    }
2992
2993    fn executor_from_materialized_store(
2994        &self,
2995        provider: Arc<CudaKernelProvider>,
2996        relation_store: &RelationStore,
2997        profiling: bool,
2998    ) -> Result<Executor> {
2999        let mut executor = Executor::new(provider.clone());
3000        executor.set_profiling(profiling);
3001        for (name, rel_id) in &self.rel_ids {
3002            executor.register_relation(*rel_id, name);
3003        }
3004
3005        for (name, schema) in &self.schemas {
3006            executor
3007                .store_mut()
3008                .put(name, provider.create_empty_buffer(schema.clone())?);
3009        }
3010
3011        for name in relation_store.names() {
3012            let buffer = relation_store.get(name).ok_or_else(|| {
3013                XlogError::Execution(format!(
3014                    "Persistent relation {} disappeared during evaluation",
3015                    name
3016                ))
3017            })?;
3018            let schema = self.schemas.get(name).ok_or_else(|| {
3019                XlogError::Execution(format!(
3020                    "Persistent relation {} not declared in program schemas",
3021                    name
3022                ))
3023            })?;
3024            ensure_schema_type_compatible(schema, buffer.schema()).map_err(|e| {
3025                XlogError::Execution(format!(
3026                    "Persistent relation {} schema mismatch: {}",
3027                    name, e
3028                ))
3029            })?;
3030            executor
3031                .store_mut()
3032                .put(name, provider.clone_buffer(buffer)?);
3033        }
3034
3035        Ok(executor)
3036    }
3037
3038    fn resident_certified_plan(&self) -> Result<Arc<ResidentGraphCertifiedPlan>> {
3039        let plan = self.ordinary_plan("resident route certification")?;
3040        self.resident_certified_plan_for_plan(plan)
3041    }
3042
3043    fn resident_certified_plan_with_outcome(
3044        &self,
3045    ) -> Result<(Arc<ResidentGraphCertifiedPlan>, bool, bool)> {
3046        let plan = self.ordinary_plan("resident route certification")?;
3047        self.resident_certified_plan_with_outcome_for_plan(plan)
3048    }
3049
3050    fn resident_certified_plan_with_outcome_for_plan(
3051        &self,
3052        plan: &ExecutionPlan,
3053    ) -> Result<(Arc<ResidentGraphCertifiedPlan>, bool, bool)> {
3054        self.reusable_state_identity
3055            .get_or_init_resident_certification_with_outcome(|| self.inspect_resident_plan(plan))
3056    }
3057
3058    fn resident_certified_plan_for_plan(
3059        &self,
3060        plan: &ExecutionPlan,
3061    ) -> Result<Arc<ResidentGraphCertifiedPlan>> {
3062        self.reusable_state_identity
3063            .get_or_init_resident_certification(|| self.inspect_resident_plan(plan))
3064    }
3065
3066    fn inspect_resident_plan(&self, plan: &ExecutionPlan) -> Result<ResidentGraphCertifiedPlan> {
3067        let resident_plan = self.resident_dependency_closed_plan(plan);
3068        let catalog = ResidentGraphSchemaCatalog::from_named_schemas(
3069            self.rel_ids.iter().filter_map(|(name, relation)| {
3070                self.schemas
3071                    .get(name)
3072                    .cloned()
3073                    .map(|schema| (name.clone(), *relation, schema))
3074            }),
3075        );
3076        ResidentGraphCertifiedPlan::inspect(Arc::new(resident_plan), &catalog)
3077    }
3078
3079    fn resident_dependency_closed_plan(&self, plan: &ExecutionPlan) -> ExecutionPlan {
3080        self.try_resident_dependency_closed_plan(plan)
3081            .unwrap_or_else(|| plan.clone())
3082    }
3083
3084    fn try_resident_dependency_closed_plan(&self, plan: &ExecutionPlan) -> Option<ExecutionPlan> {
3085        if self.program.queries.is_empty()
3086            || plan.generated_query_rules.len() != self.program.queries.len()
3087        {
3088            return None;
3089        }
3090
3091        let mut roots =
3092            Vec::with_capacity(plan.generated_query_rules.len() + self.program.constraints.len());
3093        let mut root_heads = std::collections::HashSet::with_capacity(roots.capacity());
3094        let mut seen_queries = vec![false; self.program.queries.len()];
3095        for query in &plan.generated_query_rules {
3096            let expected_head = format!("__xlog_query_{}", query.query_index);
3097            let rule = plan
3098                .rules_by_scc
3099                .get(query.scc_index)?
3100                .get(query.rule_index)?;
3101            let seen = seen_queries.get_mut(query.query_index)?;
3102            if *seen || rule.head != expected_head {
3103                return None;
3104            }
3105            let occurrences = plan
3106                .rules_by_scc
3107                .iter()
3108                .flatten()
3109                .filter(|candidate| candidate.head == expected_head)
3110                .count();
3111            if occurrences != 1 {
3112                return None;
3113            }
3114            *seen = true;
3115            roots.push(query.scc_index);
3116            if !root_heads.insert(expected_head) {
3117                return None;
3118            }
3119        }
3120        if seen_queries.iter().any(|seen| !seen) {
3121            return None;
3122        }
3123
3124        for constraint_index in 0..self.program.constraints.len() {
3125            let expected_head = format!("__xlog_constraint_{constraint_index}");
3126            let positions = plan
3127                .rules_by_scc
3128                .iter()
3129                .enumerate()
3130                .flat_map(|(scc_index, rules)| {
3131                    rules
3132                        .iter()
3133                        .filter(|rule| rule.head == expected_head)
3134                        .map(move |_| scc_index)
3135                })
3136                .collect::<Vec<_>>();
3137            let [scc_index] = positions.as_slice() else {
3138                return None;
3139            };
3140            roots.push(*scc_index);
3141            if !root_heads.insert(expected_head) {
3142                return None;
3143            }
3144        }
3145
3146        let mut defining_sccs = HashMap::new();
3147        for (scc_index, rules) in plan.rules_by_scc.iter().enumerate() {
3148            for rule in rules {
3149                let Some(relation) = self.rel_ids.get(&rule.head).copied() else {
3150                    if root_heads.contains(&rule.head) {
3151                        continue;
3152                    }
3153                    return None;
3154                };
3155                match defining_sccs.insert(relation, scc_index) {
3156                    Some(previous) if previous != scc_index => return None,
3157                    _ => {}
3158                }
3159            }
3160        }
3161
3162        plan.dependency_closed_subplan(&roots, &defining_sccs)
3163    }
3164
3165    #[cfg(test)]
3166    fn resident_certification_initializations(&self) -> u64 {
3167        self.reusable_state_identity
3168            .resident_certification_initializations()
3169    }
3170
3171    /// Evaluate the program with optional profiling
3172    ///
3173    /// # Arguments
3174    /// * `provider` - The CUDA kernel provider
3175    /// * `inputs` - Input relations
3176    /// * `profiling` - Whether to collect execution statistics
3177    pub fn evaluate_with_options(
3178        &self,
3179        provider: Arc<CudaKernelProvider>,
3180        inputs: HashMap<String, CudaBuffer>,
3181        profiling: bool,
3182    ) -> Result<LogicEvalResult> {
3183        self.reject_compiler_generated_query_relation_names(
3184            inputs.keys().map(String::as_str),
3185            "caller input",
3186        )?;
3187        let resident_mode = ResidentSelectionMode::from_env()?;
3188        if matches!(&self.plan, LogicExecutionPlan::Ordinary(_)) && resident_mode.requested() {
3189            return self.evaluate_ordinary_with_resident_mode(
3190                provider,
3191                inputs,
3192                profiling,
3193                resident_mode,
3194            );
3195        }
3196        let mut executor = self.prepare_executor(&provider, inputs, profiling)?;
3197
3198        if let LogicExecutionPlan::EpistemicG91Compatibility(g91_plan) = &self.plan {
3199            let result = self
3200                .evaluate_g91_compatibility_gpu_program(provider, executor, g91_plan, profiling)?;
3201            return self.finish_nonordinary_resident_selection(result, resident_mode);
3202        }
3203
3204        if let LogicExecutionPlan::EpistemicWfsGpu(wfs_plan) = &self.plan {
3205            let result = self.evaluate_wfs_gpu_program(provider, executor, wfs_plan, profiling)?;
3206            return self.finish_nonordinary_resident_selection(result, resident_mode);
3207        }
3208
3209        let LogicExecutionPlan::Ordinary(plan) = &self.plan else {
3210            let result = self.evaluate_epistemic_with_executor(&provider, executor, profiling)?;
3211            return self.finish_nonordinary_resident_selection(result, resident_mode);
3212        };
3213
3214        executor.execute_plan(plan)?;
3215
3216        self.enforce_constraints(&provider, &executor)?;
3217
3218        let mut queries: Vec<LogicQueryResult> = Vec::with_capacity(self.program.queries.len());
3219        for (i, query) in self.program.queries.iter().enumerate() {
3220            let internal_relation_name = format!("__xlog_query_{}", i);
3221            let buffer = executor
3222                .store_mut()
3223                .remove(&internal_relation_name)
3224                .ok_or_else(|| {
3225                    XlogError::Execution(format!(
3226                        "Missing query result relation {} (compiler bug?)",
3227                        internal_relation_name
3228                    ))
3229                })?;
3230
3231            queries.push(self.logic_query_result(
3232                provider.as_ref(),
3233                i,
3234                query,
3235                internal_relation_name,
3236                buffer,
3237            )?);
3238        }
3239
3240        // Collect execution stats if profiling was enabled
3241        let total_output_rows: u64 = queries.iter().map(|q| q.buffer.num_rows()).sum();
3242        let stats = if profiling {
3243            Some(executor.execution_stats(total_output_rows))
3244        } else {
3245            None
3246        };
3247
3248        Ok(LogicEvalResult { queries, stats })
3249    }
3250
3251    /// Build an executor seeded with declared schemas, caller inputs and program facts.
3252    ///
3253    /// Shared by ordinary evaluation and by the epistemic evidence handoff so both
3254    /// paths seed relations identically.
3255    fn prepare_executor(
3256        &self,
3257        provider: &Arc<CudaKernelProvider>,
3258        inputs: HashMap<String, CudaBuffer>,
3259        profiling: bool,
3260    ) -> Result<Executor> {
3261        self.prepare_executor_excluding_derived_placeholders(provider, inputs, profiling, None)
3262    }
3263
3264    /// Execute an epistemic program and return its accepted GPU execution evidence.
3265    ///
3266    /// `evaluate` reduces the epistemic result to query rows and drops the accepted
3267    /// world-view evidence with it. The probabilistic production adapter needs that
3268    /// evidence itself, so this entry point keeps the raw execution result.
3269    ///
3270    /// Only single-component epistemic plans are supported: split, stratified and WFS
3271    /// plans produce several world views whose probabilistic conditioning contract is
3272    /// not settled yet, so they are rejected loudly rather than silently reduced.
3273    ///
3274    /// "Ordinary" here names a plan kind, not the source. An admissible recursive modal
3275    /// program (`reach(X, Z) :- reach(X, Y), know link(Y, Z).`) is lowered by the Case-A
3276    /// reduction to ordinary recursion: the world-view machinery is erased, so no
3277    /// accepted world view survives to hand over and the program is rejected here
3278    /// despite being full of `know`. The rejection names the `epistemic_provenance`
3279    /// reduction class so the message explains the lowering instead of reading as a
3280    /// misclassification.
3281    pub fn execute_epistemic_evidence(
3282        &self,
3283        provider: Arc<CudaKernelProvider>,
3284        inputs: HashMap<String, CudaBuffer>,
3285    ) -> Result<EpistemicGpuExecutionResult> {
3286        let LogicExecutionPlan::EpistemicSingle(executable) = &self.plan else {
3287            let reduction = self
3288                .epistemic_provenance
3289                .as_ref()
3290                .map(|provenance| provenance.reduction)
3291                .unwrap_or("none");
3292            return Err(XlogError::UnsupportedEpistemicConstruct {
3293                construct: "epistemic accepted-evidence handoff".to_string(),
3294                context: format!(
3295                    "execute_epistemic_evidence requires a single-component epistemic plan; \
3296                     ordinary, split, stratified, recursive G91-compatibility and WFS plans \
3297                     are not supported (epistemic_provenance reduction: {reduction}). A \
3298                     recursive modal program reduced to ordinary recursion \
3299                     (ordinary_recursive_modal_reduction) is rejected here by design: the \
3300                     reduction erases the world-view machinery, so no accepted world view \
3301                     survives to condition on"
3302                ),
3303            });
3304        };
3305
3306        let mut executor = self.prepare_executor(&provider, inputs, false)?;
3307        let result = executor
3308            .execute_epistemic_gpu_execution(
3309                executable,
3310                capacities_for_epistemic_executable(executable)?,
3311            )
3312            .map_err(|error| self.present_epistemic_constraint_violation(error))?;
3313        result.require_runtime_dispatch_certification()?;
3314        Ok(result)
3315    }
3316
3317    /// Compare query result relations between two stores using GPU set difference.
3318    pub fn relation_stores_query_equivalent(
3319        &self,
3320        provider: &CudaKernelProvider,
3321        left: &RelationStore,
3322        right: &RelationStore,
3323    ) -> Result<bool> {
3324        for idx in 0..self.program.queries.len() {
3325            let name = format!("__xlog_query_{}", idx);
3326            let Some(left_buffer) = left.get(&name) else {
3327                return Ok(false);
3328            };
3329            let Some(right_buffer) = right.get(&name) else {
3330                return Ok(false);
3331            };
3332            if !buffers_gpu_set_equivalent(provider, left_buffer, right_buffer)? {
3333                return Ok(false);
3334            }
3335        }
3336        Ok(true)
3337    }
3338
3339    fn executor_from_relation_store(
3340        &self,
3341        provider: Arc<CudaKernelProvider>,
3342        relation_store: &RelationStore,
3343        profiling: bool,
3344    ) -> Result<Executor> {
3345        let mut executor = Executor::new(provider.clone());
3346        executor.set_profiling(profiling);
3347        for (name, rel_id) in &self.rel_ids {
3348            executor.register_relation(*rel_id, name);
3349        }
3350
3351        for (name, schema) in &self.schemas {
3352            executor
3353                .store_mut()
3354                .put(name, provider.create_empty_buffer(schema.clone())?);
3355        }
3356
3357        for name in relation_store.names() {
3358            let buffer = relation_store.get(name).ok_or_else(|| {
3359                XlogError::Execution(format!(
3360                    "Persistent relation {} disappeared during evaluation",
3361                    name
3362                ))
3363            })?;
3364            let schema = self.schemas.get(name).ok_or_else(|| {
3365                XlogError::Execution(format!(
3366                    "Persistent relation {} not declared in program schemas",
3367                    name
3368                ))
3369            })?;
3370            ensure_schema_type_compatible(schema, buffer.schema()).map_err(|e| {
3371                XlogError::Execution(format!(
3372                    "Persistent relation {} schema mismatch: {}",
3373                    name, e
3374                ))
3375            })?;
3376            executor
3377                .store_mut()
3378                .put(name, provider.clone_buffer(buffer)?);
3379        }
3380
3381        Ok(executor)
3382    }
3383
3384    fn clone_relation_store(
3385        &self,
3386        provider: &Arc<CudaKernelProvider>,
3387        source: &RelationStore,
3388    ) -> Result<RelationStore> {
3389        let mut cloned = RelationStore::new(provider.clone());
3390        for name in source.names() {
3391            let buffer = source.get(name).ok_or_else(|| {
3392                XlogError::Execution(format!("Relation {} disappeared during clone", name))
3393            })?;
3394            cloned.put(name, provider.clone_buffer(buffer)?);
3395        }
3396        Ok(cloned)
3397    }
3398
3399    fn clone_prepared_relation_snapshot(
3400        &self,
3401        provider: &Arc<CudaKernelProvider>,
3402        source: &RelationStore,
3403    ) -> Result<RelationStore> {
3404        let mut relation_names = source.names().collect::<Vec<_>>();
3405        relation_names.sort_unstable();
3406        let mut cloned = RelationStore::new(provider.clone());
3407        cloned.try_reserve_relations(relation_names.len())?;
3408        for name in relation_names {
3409            let buffer = source.get(name).ok_or_else(|| {
3410                XlogError::Execution(format!(
3411                    "Relation {name} disappeared while cloning prepared snapshot"
3412                ))
3413            })?;
3414            let context = format!("cloning prospective relation snapshot '{name}'");
3415            let cloned_buffer = provider
3416                .clone_buffer(buffer)
3417                .map_err(|error| relation_clone_error(context, error))?;
3418            cloned.put(name, cloned_buffer);
3419        }
3420        Ok(cloned)
3421    }
3422
3423    fn total_query_rows(&self, store: &RelationStore) -> Result<u64> {
3424        let mut total = 0;
3425        for i in 0..self.program.queries.len() {
3426            let relation_name = format!("__xlog_query_{}", i);
3427            let buffer = store.get(&relation_name).ok_or_else(|| {
3428                XlogError::Execution(format!(
3429                    "Missing query result relation {} (compiler bug?)",
3430                    relation_name
3431                ))
3432            })?;
3433            total += buffer.num_rows();
3434        }
3435        Ok(total)
3436    }
3437
3438    fn logic_result_from_store(
3439        &self,
3440        provider: &CudaKernelProvider,
3441        store: &RelationStore,
3442        stats: Option<ExecutionStats>,
3443    ) -> Result<LogicEvalResult> {
3444        let mut queries: Vec<LogicQueryResult> = Vec::with_capacity(self.program.queries.len());
3445        for (i, query) in self.program.queries.iter().enumerate() {
3446            let relation_name = format!("__xlog_query_{}", i);
3447            let buffer = store.get(&relation_name).ok_or_else(|| {
3448                XlogError::Execution(format!(
3449                    "Missing query result relation {} (compiler bug?)",
3450                    relation_name
3451                ))
3452            })?;
3453
3454            queries.push(self.logic_query_result(
3455                provider,
3456                i,
3457                query,
3458                relation_name,
3459                provider.clone_buffer(buffer)?,
3460            )?);
3461        }
3462
3463        Ok(LogicEvalResult { queries, stats })
3464    }
3465
3466    fn logic_query_result(
3467        &self,
3468        provider: &CudaKernelProvider,
3469        query_index: usize,
3470        query: &Query,
3471        internal_relation_name: String,
3472        buffer: CudaBuffer,
3473    ) -> Result<LogicQueryResult> {
3474        let provenance = self.epistemic_provenance.as_ref();
3475        let surface_source_query = provenance.is_some_and(|value| value.surface_source_queries);
3476        let presentation_query = if surface_source_query {
3477            self.source_program
3478                .queries
3479                .get(query_index)
3480                .ok_or_else(|| {
3481                    XlogError::Execution(format!(
3482                        "missing authored metadata for query {query_index}"
3483                    ))
3484                })?
3485        } else {
3486            query
3487        };
3488        let columns = query_output_vars(presentation_query);
3489        let buffer = if columns.is_empty() {
3490            let row_count = provider.device_row_count(&buffer)?;
3491            let row_count = u32::try_from(row_count).map_err(|_| {
3492                XlogError::Execution(format!(
3493                    "query result row count {row_count} exceeds the GPU row-count range"
3494                ))
3495            })?;
3496            provider.create_zero_arity_buffer(Schema::new(Vec::new()), row_count)?
3497        } else {
3498            buffer
3499        };
3500        let relation_name = if surface_source_query {
3501            presentation_query.atom.predicate.clone()
3502        } else {
3503            internal_relation_name
3504        };
3505
3506        Ok(LogicQueryResult {
3507            relation_name,
3508            sort_labels: columns.clone(),
3509            columns,
3510            buffer,
3511        })
3512    }
3513
3514    fn load_facts(&self, provider: &CudaKernelProvider, executor: &mut Executor) -> Result<()> {
3515        self.load_facts_into_store(provider, executor.store_mut())
3516    }
3517
3518    fn load_facts_into_store(
3519        &self,
3520        provider: &CudaKernelProvider,
3521        store: &mut RelationStore,
3522    ) -> Result<()> {
3523        let arity_qualified_predicates = if self.epistemic_provenance.is_some() {
3524            epistemic_extensional_multi_arity_predicates(&self.program)
3525        } else {
3526            predicate_arities(&self.program)
3527                .into_iter()
3528                .filter_map(|(predicate, arities)| (arities.len() > 1).then_some(predicate))
3529                .collect()
3530        };
3531        let mut rows_by_pred: HashMap<String, Vec<&[Term]>> = HashMap::new();
3532        for fact in self.program.facts() {
3533            let pred = fact.head.predicate.as_str();
3534            let arity = fact.head.terms.len();
3535            let key = if arity_qualified_predicates.contains(pred) {
3536                arity_qualified_name(pred, arity)
3537            } else {
3538                pred.to_string()
3539            };
3540            rows_by_pred.entry(key).or_default().push(&fact.head.terms);
3541        }
3542
3543        for (pred, rows) in rows_by_pred {
3544            let schema = self.schemas.get(pred.as_str()).ok_or_else(|| {
3545                XlogError::Execution(format!(
3546                    "Missing inferred schema for fact predicate {}",
3547                    pred
3548                ))
3549            })?;
3550
3551            if rows.iter().any(|r| r.len() != schema.arity()) {
3552                return Err(XlogError::Execution(format!(
3553                    "Fact arity mismatch for {} (expected {} columns)",
3554                    pred,
3555                    schema.arity()
3556                )));
3557            }
3558
3559            let mut columns: Vec<Vec<u8>> = vec![Vec::new(); schema.arity()];
3560            for row in rows {
3561                for (col_idx, term) in row.iter().enumerate() {
3562                    let typ = schema.column_type(col_idx).ok_or_else(|| {
3563                        XlogError::Execution(format!("Missing type for column {}", col_idx))
3564                    })?;
3565                    append_ground_term_bytes(&mut columns[col_idx], term, typ).map_err(|error| {
3566                        XlogError::Execution(format!(
3567                            "Failed to encode fact for predicate {pred} at column {col_idx}: {error}"
3568                        ))
3569                    })?;
3570                }
3571            }
3572
3573            let fact_buf = if schema.arity() == 0 {
3574                // Nullary predicate: every `pred().` assertion denotes the same unit
3575                // tuple `()`, so presence is a single row. `create_buffer_from_slices`
3576                // with no column slices yields a 0-row (absent) relation, which would
3577                // make an asserted nullary fact read as false everywhere downstream
3578                // (ordinary joins and epistemic modal membership alike).
3579                provider.create_zero_arity_buffer(schema.clone(), 1)?
3580            } else {
3581                let slices: Vec<&[u8]> = columns.iter().map(|c| c.as_slice()).collect();
3582                provider.create_buffer_from_slices(&slices, schema.clone())?
3583            };
3584
3585            let existing = store.get(&pred).ok_or_else(|| {
3586                XlogError::Execution(format!(
3587                    "Missing base relation {} while loading facts",
3588                    pred
3589                ))
3590            })?;
3591
3592            let merged = provider.union(existing, &fact_buf)?;
3593            store.put(pred.as_str(), merged);
3594        }
3595
3596        Ok(())
3597    }
3598
3599    fn evaluate_wfs_gpu_program(
3600        &self,
3601        provider: Arc<CudaKernelProvider>,
3602        base_executor: Executor,
3603        wfs: &EpistemicWfsGpuPlan,
3604        profiling: bool,
3605    ) -> Result<LogicEvalResult> {
3606        let base_store = self.clone_relation_store(&provider, base_executor.store())?;
3607        let mut stats = profiling.then(ExecutionStats::default);
3608        let lower_store =
3609            self.run_wfs_gpu_fixpoint(&provider, &base_store, wfs, profiling, &mut stats)?;
3610        self.enforce_constraints_in_store(provider.as_ref(), &lower_store)?;
3611        let total_output_rows = self.total_query_rows(&lower_store)?;
3612        finalize_iterative_execution_stats(&mut stats, total_output_rows);
3613        self.logic_result_from_store(provider.as_ref(), &lower_store, stats)
3614    }
3615
3616    fn run_wfs_gpu_fixpoint(
3617        &self,
3618        provider: &Arc<CudaKernelProvider>,
3619        base_store: &RelationStore,
3620        wfs: &EpistemicWfsGpuPlan,
3621        profiling: bool,
3622        stats: &mut Option<ExecutionStats>,
3623    ) -> Result<RelationStore> {
3624        let upper_executor =
3625            self.run_gpu_ordinary_pass(provider, &wfs.overapprox, base_store, &[], profiling)?;
3626        collect_iterative_execution_stats(stats, &upper_executor);
3627        let mut upper_store = self.clone_relation_store(provider, upper_executor.store())?;
3628        let mut lower_store = self.clone_relation_store(provider, base_store)?;
3629
3630        for _ in 0..wfs.max_iterations {
3631            let upper_fixed: Vec<_> = wfs
3632                .upper_fixed_names
3633                .iter()
3634                .map(|(source, fixed)| (source.as_str(), fixed.as_str(), &upper_store))
3635                .collect();
3636            let lower_executor = self.run_gpu_ordinary_pass(
3637                provider,
3638                &wfs.lower,
3639                base_store,
3640                &upper_fixed,
3641                profiling,
3642            )?;
3643            collect_iterative_execution_stats(stats, &lower_executor);
3644            let next_lower = self.clone_relation_store(provider, lower_executor.store())?;
3645
3646            let lower_fixed: Vec<_> = wfs
3647                .lower_fixed_names
3648                .iter()
3649                .map(|(source, fixed)| (source.as_str(), fixed.as_str(), &next_lower))
3650                .collect();
3651            let next_upper_executor = self.run_gpu_ordinary_pass(
3652                provider,
3653                &wfs.upper,
3654                base_store,
3655                &lower_fixed,
3656                profiling,
3657            )?;
3658            collect_iterative_execution_stats(stats, &next_upper_executor);
3659            let next_upper = self.clone_relation_store(provider, next_upper_executor.store())?;
3660
3661            let lower_converged =
3662                self.wfs_gpu_stores_equivalent(provider, wfs, &lower_store, &next_lower)?;
3663            let upper_converged =
3664                self.wfs_gpu_stores_equivalent(provider, wfs, &upper_store, &next_upper)?;
3665            lower_store = next_lower;
3666            upper_store = next_upper;
3667            if lower_converged && upper_converged {
3668                return Ok(lower_store);
3669            }
3670        }
3671
3672        Err(XlogError::Execution(format!(
3673            "GPU-backed WFS did not converge within {} alternating-fixpoint iterations; raise \
3674             #pragma max_recursion_depth only when the finite relation domain requires it",
3675            wfs.max_iterations
3676        )))
3677    }
3678
3679    fn evaluate_g91_compatibility_gpu_program(
3680        &self,
3681        provider: Arc<CudaKernelProvider>,
3682        base_executor: Executor,
3683        g91: &EpistemicG91CompatibilityGpuPlan,
3684        profiling: bool,
3685    ) -> Result<LogicEvalResult> {
3686        let base_store = self.clone_relation_store(&provider, base_executor.store())?;
3687        let mut stats = profiling.then(ExecutionStats::default);
3688        let mut current_store = self.run_gpu_evaluation_pass(
3689            &provider,
3690            &g91.upper_bound,
3691            &base_store,
3692            &[],
3693            profiling,
3694            &mut stats,
3695        )?;
3696        let refinement_schemas = gpu_evaluation_pass_schemas(&g91.refinement);
3697
3698        for _ in 0..g91.max_iterations {
3699            let snapshots = g91
3700                .snapshot_relations
3701                .iter()
3702                .map(|(source, snapshot)| (source.as_str(), snapshot.as_str(), &current_store))
3703                .collect::<Vec<_>>();
3704            let next_store = self.run_gpu_evaluation_pass(
3705                &provider,
3706                &g91.refinement,
3707                &base_store,
3708                &snapshots,
3709                profiling,
3710                &mut stats,
3711            )?;
3712            let converged = self.gpu_stores_equivalent(
3713                &provider,
3714                &refinement_schemas,
3715                &g91.convergence_predicates,
3716                &current_store,
3717                &next_store,
3718            )?;
3719            if converged {
3720                self.enforce_constraints_in_store(&provider, &next_store)?;
3721                let total_output_rows = self.total_query_rows(&next_store)?;
3722                finalize_iterative_execution_stats(&mut stats, total_output_rows);
3723                return self.logic_result_from_store(provider.as_ref(), &next_store, stats);
3724            }
3725            current_store = next_store;
3726        }
3727
3728        Err(XlogError::Execution(format!(
3729            "Gelfond-1991 tuple compatibility did not converge within {} refinement iterations; \
3730             raise #pragma max_recursion_depth only when the finite relation domain requires it",
3731            g91.max_iterations
3732        )))
3733    }
3734
3735    fn run_gpu_ordinary_pass(
3736        &self,
3737        provider: &Arc<CudaKernelProvider>,
3738        pass: &GpuOrdinaryPass,
3739        base_store: &RelationStore,
3740        fixed_relations: &[(&str, &str, &RelationStore)],
3741        profiling: bool,
3742    ) -> Result<Executor> {
3743        let mut executor = Executor::new(provider.clone());
3744        executor.set_profiling(profiling);
3745        for (name, rel_id) in &pass.rel_ids {
3746            executor.register_relation(*rel_id, name);
3747        }
3748        for (name, schema) in &pass.schemas {
3749            executor
3750                .store_mut()
3751                .put(name, provider.create_empty_buffer(schema.clone())?);
3752        }
3753        for name in base_store.names() {
3754            // Query and constraint relations are outputs of this compiled pass.
3755            // Seeding them from an earlier epistemic stratum would union stale,
3756            // ungated candidates into the authoritative recomputation.
3757            if pass.schemas.contains_key(name)
3758                && !name.starts_with("__xlog_query_")
3759                && !name.starts_with("__xlog_constraint_")
3760            {
3761                let buffer = base_store.get(name).ok_or_else(|| {
3762                    XlogError::Execution(format!("WFS base relation {name} disappeared"))
3763                })?;
3764                executor
3765                    .store_mut()
3766                    .put(name, provider.clone_buffer(buffer)?);
3767            }
3768        }
3769        for &(source, fixed, source_store) in fixed_relations {
3770            let buffer =
3771                self.gpu_clone_or_empty(provider, &pass.schemas, source, fixed, source_store)?;
3772            executor.store_mut().put(fixed, buffer);
3773        }
3774        executor.execute_plan(&pass.plan)?;
3775        Ok(executor)
3776    }
3777
3778    fn run_gpu_evaluation_pass(
3779        &self,
3780        provider: &Arc<CudaKernelProvider>,
3781        pass: &GpuEvaluationPass,
3782        base_store: &RelationStore,
3783        fixed_relations: &[(&str, &str, &RelationStore)],
3784        profiling: bool,
3785        stats: &mut Option<ExecutionStats>,
3786    ) -> Result<RelationStore> {
3787        match pass {
3788            GpuEvaluationPass::Ordinary(ordinary) => {
3789                let executor = self.run_gpu_ordinary_pass(
3790                    provider,
3791                    ordinary,
3792                    base_store,
3793                    fixed_relations,
3794                    profiling,
3795                )?;
3796                collect_iterative_execution_stats(stats, &executor);
3797                self.clone_relation_store(provider, executor.store())
3798            }
3799            GpuEvaluationPass::Wfs(wfs) => {
3800                let schemas = wfs_plan_combined_schemas(wfs);
3801                let mut pass_base = self.clone_relation_store(provider, base_store)?;
3802                for &(source, fixed, source_store) in fixed_relations {
3803                    let buffer =
3804                        self.gpu_clone_or_empty(provider, &schemas, source, fixed, source_store)?;
3805                    pass_base.put(fixed, buffer);
3806                }
3807                self.run_wfs_gpu_fixpoint(provider, &pass_base, wfs, profiling, stats)
3808            }
3809        }
3810    }
3811
3812    fn gpu_clone_or_empty(
3813        &self,
3814        provider: &Arc<CudaKernelProvider>,
3815        schemas: &HashMap<String, Schema>,
3816        source_name: &str,
3817        target_name: &str,
3818        store: &RelationStore,
3819    ) -> Result<CudaBuffer> {
3820        let target_schema = schemas
3821            .get(target_name)
3822            .or_else(|| self.schemas.get(target_name))
3823            .ok_or_else(|| {
3824                XlogError::Execution(format!(
3825                    "missing iterative GPU relation schema for {target_name}"
3826                ))
3827            })?;
3828        if let Some(buffer) = store.get(source_name) {
3829            // Nullary fixed relations use a unary unit marker inside WFS passes.
3830            // CUDA anti-join kernels then distinguish the absent and present unit
3831            // tuples using their ordinary nonzero-arity path.
3832            if buffer.schema().arity() == 0 && target_schema.arity() == 1 {
3833                if provider.device_row_count(buffer)? == 0 {
3834                    return provider.create_empty_buffer(target_schema.clone());
3835                }
3836                let marker = 1u32.to_le_bytes();
3837                return provider
3838                    .create_buffer_from_slices(&[marker.as_slice()], target_schema.clone());
3839            }
3840            return provider.clone_buffer(buffer);
3841        }
3842        provider.create_empty_buffer(target_schema.clone())
3843    }
3844
3845    fn wfs_gpu_stores_equivalent(
3846        &self,
3847        provider: &Arc<CudaKernelProvider>,
3848        wfs: &EpistemicWfsGpuPlan,
3849        left: &RelationStore,
3850        right: &RelationStore,
3851    ) -> Result<bool> {
3852        self.gpu_stores_equivalent(
3853            provider,
3854            &wfs.lower.schemas,
3855            &wfs.intensional_predicates,
3856            left,
3857            right,
3858        )
3859    }
3860
3861    fn gpu_stores_equivalent(
3862        &self,
3863        provider: &Arc<CudaKernelProvider>,
3864        schemas: &HashMap<String, Schema>,
3865        predicates: &[String],
3866        left: &RelationStore,
3867        right: &RelationStore,
3868    ) -> Result<bool> {
3869        for pred in predicates {
3870            let left_buf = self.gpu_clone_or_empty(provider, schemas, pred, pred, left)?;
3871            let right_buf = self.gpu_clone_or_empty(provider, schemas, pred, pred, right)?;
3872            if !buffers_gpu_set_equivalent(provider.as_ref(), &left_buf, &right_buf)? {
3873                return Ok(false);
3874            }
3875        }
3876        Ok(true)
3877    }
3878
3879    fn ordinary_plan(&self, context: &str) -> Result<&ExecutionPlan> {
3880        match &self.plan {
3881            LogicExecutionPlan::Ordinary(plan) => Ok(plan),
3882            LogicExecutionPlan::EpistemicG91Compatibility(_)
3883            | LogicExecutionPlan::EpistemicWfsGpu(_)
3884            | LogicExecutionPlan::EpistemicSingle(_)
3885            | LogicExecutionPlan::EpistemicSplit(_)
3886            | LogicExecutionPlan::EpistemicStratified(_) => {
3887                Err(XlogError::UnsupportedEpistemicConstruct {
3888                    construct: "epistemic high-level persistent execution".to_string(),
3889                    context: format!(
3890                        "{context} requires an ordinary RIR plan; use evaluate/evaluate_with_options \
3891                         for production epistemic GPU dispatch"
3892                    ),
3893                })
3894            }
3895        }
3896    }
3897
3898    fn evaluate_epistemic_with_executor(
3899        &self,
3900        provider: &Arc<CudaKernelProvider>,
3901        mut executor: Executor,
3902        profiling: bool,
3903    ) -> Result<LogicEvalResult> {
3904        let mut queries = Vec::new();
3905        let mut accumulated_stats = None;
3906        match &self.plan {
3907            LogicExecutionPlan::EpistemicSingle(executable) => {
3908                let result = executor
3909                    .execute_epistemic_gpu_execution(
3910                        executable,
3911                        capacities_for_epistemic_executable(executable)?,
3912                    )
3913                    .map_err(|error| self.present_epistemic_constraint_violation(error))?;
3914                result.require_runtime_dispatch_certification()?;
3915                queries.extend(epistemic_result_to_query_results(
3916                    epistemic_output_relation_name(executable)?,
3917                    result,
3918                ));
3919            }
3920            LogicExecutionPlan::EpistemicSplit(split) => {
3921                let executables: Vec<_> = split
3922                    .components
3923                    .iter()
3924                    .map(|component| &component.executable)
3925                    .collect();
3926                let batch = executor
3927                    .execute_epistemic_gpu_execution_batch_with_trace(
3928                        &executables,
3929                        capacities_for_epistemic_split(split)?,
3930                    )
3931                    .map_err(|error| self.present_epistemic_constraint_violation(error))?;
3932                batch
3933                    .require_trace_matches_components("xlog high-level epistemic GPU execution")?;
3934                for result in &batch.results {
3935                    result.require_runtime_dispatch_certification()?;
3936                }
3937                for (component, result) in split.components.iter().zip(batch.results) {
3938                    // A JOINT-SOLVED coalesced multi-head component yields one query
3939                    // per coupled head: the primary head from `final_output` plus
3940                    // each additional head materialized against the SAME accepted
3941                    // world view. Single-head components yield exactly one query.
3942                    queries.extend(epistemic_result_to_query_results(
3943                        epistemic_output_relation_name(&component.executable)?,
3944                        result,
3945                    ));
3946                }
3947            }
3948            LogicExecutionPlan::EpistemicStratified(stratified) => {
3949                // Execute strata in topological order on the SAME executor. After
3950                // each stratum, write its GATED head output(s) into the store as
3951                // base relations so the NEXT stratum's `know`/`possible` over a
3952                // lower head reads the gated extension through the existing tuple-key
3953                // membership filter (or, once the head is a materialized base
3954                // relation, Case-A resolve-into-body) — never double-gating against
3955                // a still-modal relation.
3956                //
3957                // Every authored query is evaluated by its compiled query rule only
3958                // after the modal heads and ordinary closure are final. This preserves
3959                // source order, constants, repeated-variable filters, projections and
3960                // logical zero-column truth without exposing whole head relations.
3961                let has_authored_queries = !self.program.queries.is_empty();
3962                let stratum_count = stratified.strata.len();
3963                for (stratum_index, stratum) in stratified.strata.iter().enumerate() {
3964                    let is_last = stratum_index + 1 == stratum_count;
3965                    match &stratum.plan {
3966                        StratumPlanKind::Single(executable) => {
3967                            let result = executor
3968                                .execute_epistemic_gpu_execution(
3969                                    executable,
3970                                    capacities_for_epistemic_executable(executable)?,
3971                                )
3972                                .map_err(|error| {
3973                                    self.present_epistemic_constraint_violation(error)
3974                                })?;
3975                            result.require_runtime_dispatch_certification()?;
3976                            let primary_head = epistemic_output_relation_name(executable)?;
3977                            Self::materialize_epistemic_stratum_result(
3978                                &mut executor,
3979                                primary_head,
3980                                result,
3981                                is_last && !has_authored_queries,
3982                                &mut queries,
3983                            )?;
3984                        }
3985                        StratumPlanKind::Split(split) => {
3986                            let executables: Vec<_> = split
3987                                .components
3988                                .iter()
3989                                .map(|component| &component.executable)
3990                                .collect();
3991                            let batch = executor
3992                                .execute_epistemic_gpu_execution_batch_with_trace(
3993                                    &executables,
3994                                    capacities_for_epistemic_split(split)?,
3995                                )
3996                                .map_err(|error| {
3997                                    self.present_epistemic_constraint_violation(error)
3998                                })?;
3999                            batch.require_trace_matches_components(
4000                                "xlog high-level stratified epistemic GPU execution",
4001                            )?;
4002                            for result in &batch.results {
4003                                result.require_runtime_dispatch_certification()?;
4004                            }
4005                            let primaries: Vec<String> = split
4006                                .components
4007                                .iter()
4008                                .map(|component| {
4009                                    epistemic_output_relation_name(&component.executable)
4010                                })
4011                                .collect::<Result<Vec<_>>>()?;
4012                            for (primary_head, result) in primaries.into_iter().zip(batch.results) {
4013                                Self::materialize_epistemic_stratum_result(
4014                                    &mut executor,
4015                                    primary_head,
4016                                    result,
4017                                    is_last && !has_authored_queries,
4018                                    &mut queries,
4019                                )?;
4020                            }
4021                        }
4022                        StratumPlanKind::Ordinary {
4023                            plan,
4024                            head_predicates,
4025                        } => {
4026                            // Case-A recursive stratum over the materialized base
4027                            // determined head: the ordinary semi-naive engine writes
4028                            // the (correctly gated) head relation into the store.
4029                            executor.execute_plan(plan)?;
4030                            for head in head_predicates {
4031                                if is_last && !has_authored_queries {
4032                                    let buffer =
4033                                        executor.store().get(head.as_str()).ok_or_else(|| {
4034                                            XlogError::Execution(format!(
4035                                            "missing stratified ordinary stratum output relation \
4036                                             {head}"
4037                                        ))
4038                                        })?;
4039                                    let cloned = executor.clone_store_relation(buffer)?;
4040                                    queries.push(epistemic_buffer_to_query_result(
4041                                        head.clone(),
4042                                        cloned,
4043                                    ));
4044                                }
4045                            }
4046                        }
4047                    }
4048                }
4049
4050                // Compiler-local relation IDs cannot safely be registered into the
4051                // modal executor. Run the ordinary closure, authored query rules and
4052                // generated constraint relations in an isolated executor seeded from
4053                // the final gated store, then make that store authoritative.
4054                if profiling {
4055                    accumulated_stats = Some(executor.execution_stats(0));
4056                }
4057                executor = self.run_gpu_ordinary_pass(
4058                    provider,
4059                    &stratified.ordinary_post,
4060                    executor.store(),
4061                    &[],
4062                    profiling,
4063                )?;
4064                if profiling {
4065                    collect_iterative_execution_stats(&mut accumulated_stats, &executor);
4066                }
4067                for (query_index, query) in self.program.queries.iter().enumerate() {
4068                    let internal_relation_name = format!("__xlog_query_{query_index}");
4069                    let buffer = executor
4070                        .store_mut()
4071                        .remove(&internal_relation_name)
4072                        .ok_or_else(|| {
4073                            XlogError::Execution(format!(
4074                                "missing stratified post-stage query relation \
4075                                 {internal_relation_name}"
4076                            ))
4077                        })?;
4078                    queries.push(self.logic_query_result(
4079                        provider,
4080                        query_index,
4081                        query,
4082                        internal_relation_name,
4083                        buffer,
4084                    )?);
4085                }
4086            }
4087            LogicExecutionPlan::EpistemicG91Compatibility(_)
4088            | LogicExecutionPlan::EpistemicWfsGpu(_) => {
4089                unreachable!("iterative GPU epistemic plans are handled earlier")
4090            }
4091            LogicExecutionPlan::Ordinary(_) => {
4092                unreachable!("ordinary plans are handled earlier")
4093            }
4094        }
4095
4096        self.enforce_constraints_in_store(provider, executor.store())?;
4097        let total_output_rows: u64 = queries.iter().map(|q| q.buffer.num_rows()).sum();
4098        let stats = if profiling {
4099            if let Some(mut stats) = accumulated_stats {
4100                stats.total_output_rows = total_output_rows;
4101                Some(stats)
4102            } else {
4103                Some(executor.execution_stats(total_output_rows))
4104            }
4105        } else {
4106            None
4107        };
4108        Ok(LogicEvalResult { queries, stats })
4109    }
4110
4111    /// Materialize one epistemic stratum result's GATED head(s) into the store.
4112    ///
4113    /// Every gated head (primary `final_output` plus joint additional heads) is
4114    /// written to the store so higher strata can gate against it. Explicit authored
4115    /// queries are projected from this store after all strata and the ordinary post
4116    /// stage complete.
4117    fn materialize_epistemic_stratum_result(
4118        executor: &mut Executor,
4119        primary_head: String,
4120        result: EpistemicGpuExecutionResult,
4121        surface_default_results: bool,
4122        queries: &mut Vec<LogicQueryResult>,
4123    ) -> Result<()> {
4124        executor.materialize_epistemic_head_relation(&primary_head, &result.final_output)?;
4125        for (head, buffer) in &result.additional_head_outputs {
4126            executor.materialize_epistemic_head_relation(head, buffer)?;
4127        }
4128        if surface_default_results {
4129            queries.extend(epistemic_result_to_query_results(primary_head, result));
4130        }
4131        Ok(())
4132    }
4133
4134    fn enforce_constraints(
4135        &self,
4136        provider: &CudaKernelProvider,
4137        executor: &Executor,
4138    ) -> Result<()> {
4139        self.enforce_constraints_in_store(provider, executor.store())
4140    }
4141
4142    fn constraint_violation_error(&self, constraint_index: usize) -> XlogError {
4143        let presentation_constraint = self
4144            .authored_constraints
4145            .as_ref()
4146            .and_then(|constraints| {
4147                constraints
4148                    .iter()
4149                    .find(|constraint| constraint.authored_index == Some(constraint_index))
4150            })
4151            .or_else(|| {
4152                self.source_program
4153                    .constraints
4154                    .iter()
4155                    .find(|constraint| constraint.authored_index == Some(constraint_index))
4156            })
4157            .or_else(|| {
4158                self.program
4159                    .constraints
4160                    .iter()
4161                    .find(|constraint| constraint.authored_index == Some(constraint_index))
4162            });
4163        let Some(presentation_constraint) = presentation_constraint else {
4164            return XlogError::Execution(format!("Constraint {constraint_index} violated"));
4165        };
4166        XlogError::Execution(format!(
4167            "Constraint {} violated: {}",
4168            constraint_index,
4169            format_constraint_body(&presentation_constraint.body)
4170        ))
4171    }
4172
4173    fn present_epistemic_constraint_violation(&self, error: XlogError) -> XlogError {
4174        match error {
4175            XlogError::ConstraintViolation {
4176                constraint_index, ..
4177            } => self.constraint_violation_error(constraint_index),
4178            other => other,
4179        }
4180    }
4181
4182    fn enforce_constraints_in_store(
4183        &self,
4184        provider: &CudaKernelProvider,
4185        store: &RelationStore,
4186    ) -> Result<()> {
4187        for constraint in &self.program.constraints {
4188            if constraint
4189                .body
4190                .iter()
4191                .any(|literal| matches!(literal, BodyLiteral::Epistemic(_)))
4192            {
4193                continue;
4194            }
4195            let i = constraint.authored_index.ok_or_else(|| {
4196                XlogError::Execution(
4197                    "ordinary constraint reached execution without an authored identity"
4198                        .to_string(),
4199                )
4200            })?;
4201            let name = format!("__xlog_constraint_{i}");
4202            let buf = store.get(&name).ok_or_else(|| {
4203                XlogError::Execution(format!(
4204                    "Missing constraint result relation {} (compiler bug?)",
4205                    name
4206                ))
4207            })?;
4208
4209            if buf.num_rows() == 0 {
4210                continue;
4211            }
4212
4213            let rows = provider.download_column::<u32>(buf, 0).unwrap_or_default();
4214            if rows.is_empty() {
4215                continue;
4216            }
4217
4218            return Err(self.constraint_violation_error(i));
4219        }
4220
4221        Ok(())
4222    }
4223}
4224
4225fn collect_iterative_execution_stats(stats: &mut Option<ExecutionStats>, executor: &Executor) {
4226    let Some(combined) = stats.as_mut() else {
4227        return;
4228    };
4229    let mut pass = executor.execution_stats(0);
4230    let stratum_offset = combined.strata.len();
4231    for (index, stratum) in pass.strata.iter_mut().enumerate() {
4232        stratum.stratum_id = stratum_offset + index;
4233    }
4234    combined.total_duration_us = combined
4235        .total_duration_us
4236        .saturating_add(pass.total_duration_us);
4237    combined.peak_memory_bytes = combined.peak_memory_bytes.max(pass.peak_memory_bytes);
4238    combined.memory_budget_bytes = combined.memory_budget_bytes.max(pass.memory_budget_bytes);
4239    combined.wcoj_triangle_dispatch_count = combined
4240        .wcoj_triangle_dispatch_count
4241        .saturating_add(pass.wcoj_triangle_dispatch_count);
4242    combined.wcoj_4cycle_dispatch_count = combined
4243        .wcoj_4cycle_dispatch_count
4244        .saturating_add(pass.wcoj_4cycle_dispatch_count);
4245    combined.wcoj_groupby_fusion_dispatch_count = combined
4246        .wcoj_groupby_fusion_dispatch_count
4247        .saturating_add(pass.wcoj_groupby_fusion_dispatch_count);
4248    combined.free_join_dispatch_count = combined
4249        .free_join_dispatch_count
4250        .saturating_add(pass.free_join_dispatch_count);
4251    combined.factorized_delta_dispatch_count = combined
4252        .factorized_delta_dispatch_count
4253        .saturating_add(pass.factorized_delta_dispatch_count);
4254    combined.wcoj_error_decline_count = combined
4255        .wcoj_error_decline_count
4256        .saturating_add(pass.wcoj_error_decline_count);
4257    combined.strata.append(&mut pass.strata);
4258}
4259
4260fn finalize_iterative_execution_stats(stats: &mut Option<ExecutionStats>, total_output_rows: u64) {
4261    if let Some(stats) = stats {
4262        stats.total_output_rows = total_output_rows;
4263    }
4264}
4265
4266const DEFAULT_EPISTEMIC_MAX_MODELS_PER_REDUCTION: usize = 1024;
4267
4268/// Normalize a parsed program through the pre-compilation passes used by execution.
4269///
4270/// This helper does not resolve imports; import-aware callers merge them first. It
4271/// expands user-defined functions with the entry program's recursion limit, normalizes
4272/// meta and list builtins, and desugars shared-variable epistemic constraints.
4273pub fn normalize_program_for_execution(mut program: Program) -> Result<Program> {
4274    if program.authored_constraint_source_bound.is_some() {
4275        program.validate_prepared_authored_constraint_identity()?;
4276    } else {
4277        program.prepare_authored_constraint_identity_at_root()?;
4278    }
4279    let max_recursion = program.directives.max_recursion_depth_or_default();
4280    let expanded = xlog_logic::expand_program_functions_owned(program, max_recursion)
4281        .map_err(|e| XlogError::Compilation(e.to_string()))?;
4282    let normalized = xlog_logic::normalize_meta_builtins_owned(expanded)?;
4283    let listed = xlog_logic::normalize_list_builtins_owned(normalized)?;
4284    Ok(desugar_shared_variable_epistemic_constraints(listed))
4285}
4286
4287enum WfsNegationTransform<'a> {
4288    Drop,
4289    Rename {
4290        names: &'a HashMap<String, String>,
4291        source_schemas: &'a HashMap<String, Schema>,
4292    },
4293}
4294
4295fn compile_g91_compatibility_gpu_plan(
4296    reduction: &G91CompatibilityReduction,
4297) -> Result<EpistemicG91CompatibilityGpuPlan> {
4298    let upper_bound = compile_gpu_evaluation_pass(reduction.upper_bound_program())?;
4299    let upper_schemas = gpu_evaluation_pass_schemas(&upper_bound);
4300    let mut refinement_program = reduction.refinement_program().clone();
4301    add_inferred_g91_snapshot_declarations(
4302        &mut refinement_program,
4303        reduction.snapshot_relations(),
4304        &upper_schemas,
4305    )?;
4306    let max_iterations = (refinement_program
4307        .directives
4308        .max_recursion_depth_or_default() as usize)
4309        .max(1);
4310    let refinement = compile_gpu_evaluation_pass(&refinement_program)?;
4311    Ok(EpistemicG91CompatibilityGpuPlan {
4312        upper_bound,
4313        refinement,
4314        snapshot_relations: reduction.snapshot_relations().clone(),
4315        convergence_predicates: reduction.convergence_predicates().to_vec(),
4316        max_iterations,
4317    })
4318}
4319
4320fn compile_gpu_evaluation_pass(program: &Program) -> Result<GpuEvaluationPass> {
4321    let stratification = xlog_logic::stratify::analyze_stratification(program);
4322    if stratification.non_monotone_sccs.is_empty() {
4323        Ok(GpuEvaluationPass::Ordinary(Box::new(
4324            compile_gpu_ordinary_pass(program)?,
4325        )))
4326    } else {
4327        Ok(GpuEvaluationPass::Wfs(Box::new(
4328            compile_epistemic_wfs_gpu_plan(program)?,
4329        )))
4330    }
4331}
4332
4333fn add_inferred_g91_snapshot_declarations(
4334    refinement: &mut Program,
4335    snapshots: &BTreeMap<String, String>,
4336    upper_schemas: &HashMap<String, Schema>,
4337) -> Result<()> {
4338    let existing = refinement
4339        .predicates
4340        .iter()
4341        .map(|declaration| declaration.name.clone())
4342        .collect::<BTreeSet<_>>();
4343    let mut inferred = Vec::new();
4344    for (source, snapshot) in snapshots {
4345        if existing.contains(snapshot) {
4346            continue;
4347        }
4348        let schema =
4349            upper_schemas
4350                .get(source)
4351                .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
4352                    construct: "Gelfond-1991 compatibility snapshot schema".to_string(),
4353                    context: format!(
4354                        "upper-bound compilation produced no schema for compatibility relation \
4355                     `{source}`"
4356                    ),
4357                })?;
4358        let columns = schema
4359            .columns
4360            .iter()
4361            .map(|(name, scalar_type)| PredColumn {
4362                name: Some(name.clone()),
4363                typ: TypeRef::Scalar(*scalar_type),
4364            })
4365            .collect::<Vec<_>>();
4366        inferred.push(PredDecl {
4367            name: snapshot.clone(),
4368            types: columns.iter().map(|column| column.typ.clone()).collect(),
4369            columns,
4370            is_private: false,
4371        });
4372    }
4373    refinement.predicates.extend(inferred);
4374    Ok(())
4375}
4376
4377fn compile_epistemic_wfs_gpu_plan(program: &Program) -> Result<EpistemicWfsGpuPlan> {
4378    let negated = wfs_negated_predicates(program);
4379    let upper_fixed_names = wfs_fixed_names(program, &negated, "__wfs_upper");
4380    let lower_fixed_names = wfs_fixed_names(program, &negated, "__wfs_lower");
4381    let source_schemas = infer_wfs_source_schemas(program)?;
4382
4383    let mut overapprox_program = wfs_transform_program(program, WfsNegationTransform::Drop)?;
4384    // Constraints do not influence the alternating fixpoint. Evaluate them only in
4385    // the lower pass, where positive atoms read the true extension and negated atoms
4386    // read the frozen upper extension.
4387    overapprox_program.constraints.clear();
4388    let lower_program = wfs_transform_program(
4389        program,
4390        WfsNegationTransform::Rename {
4391            names: &upper_fixed_names,
4392            source_schemas: &source_schemas,
4393        },
4394    )?;
4395    let mut upper_program = wfs_transform_program(
4396        program,
4397        WfsNegationTransform::Rename {
4398            names: &lower_fixed_names,
4399            source_schemas: &source_schemas,
4400        },
4401    )?;
4402    upper_program.constraints.clear();
4403
4404    Ok(EpistemicWfsGpuPlan {
4405        overapprox: compile_gpu_ordinary_pass(&overapprox_program)?,
4406        lower: compile_gpu_ordinary_pass(&lower_program)?,
4407        upper: compile_gpu_ordinary_pass(&upper_program)?,
4408        intensional_predicates: wfs_intensional_predicates(program),
4409        upper_fixed_names,
4410        lower_fixed_names,
4411        max_iterations: (program.directives.max_recursion_depth_or_default() as usize).max(1),
4412    })
4413}
4414
4415fn infer_wfs_source_schemas(program: &Program) -> Result<HashMap<String, Schema>> {
4416    // Ordinary compilation cannot plan a cycle through negation, but its schema
4417    // inference treats positive and negated atoms identically. Make a monotone copy
4418    // that retains every atom, compile it through the authoritative compiler path,
4419    // and use the resulting schemas for the private fixed relations.
4420    let mut inference_program = program.clone();
4421    for rule in &mut inference_program.rules {
4422        for literal in &mut rule.body {
4423            if let BodyLiteral::Negated(atom) = literal {
4424                *literal = BodyLiteral::Positive(atom.clone());
4425            }
4426        }
4427    }
4428
4429    let mut compiler = Compiler::new();
4430    compiler.compile_prepared_program(&inference_program)?;
4431    Ok(compiler.schemas().clone())
4432}
4433
4434fn compile_gpu_ordinary_pass(program: &Program) -> Result<GpuOrdinaryPass> {
4435    let mut compiler = Compiler::new();
4436    let plan = compiler.compile_prepared_program(program)?;
4437    Ok(GpuOrdinaryPass {
4438        plan,
4439        schemas: compiler.schemas().clone(),
4440        rel_ids: compiler.rel_ids().clone(),
4441    })
4442}
4443
4444fn wfs_transform_program(program: &Program, negation: WfsNegationTransform<'_>) -> Result<Program> {
4445    let mut out = program.clone();
4446    out.rules = program
4447        .rules
4448        .iter()
4449        .map(|rule| {
4450            let mut rule = rule.clone();
4451            let was_fact = rule.body.is_empty();
4452            let mut body = transform_wfs_body(&rule.body, &negation)?;
4453            if !was_fact && body.is_empty() {
4454                // Dropping every negated literal computes the WFS upper
4455                // over-approximation. Keep the transformed clause executable as a
4456                // unit-derived rule; an empty body would otherwise be reclassified as
4457                // an extensional fact and never derived by the pass.
4458                body.push(BodyLiteral::Comparison(xlog_logic::ast::Comparison {
4459                    left: Term::Integer(1),
4460                    op: xlog_logic::ast::CompOp::Eq,
4461                    right: Term::Integer(1),
4462                }));
4463            }
4464            rule.body = body;
4465            Ok(rule)
4466        })
4467        .collect::<Result<Vec<_>>>()?;
4468    out.constraints = program
4469        .constraints
4470        .iter()
4471        .map(|constraint| {
4472            let mut constraint = constraint.clone();
4473            constraint.body = transform_wfs_body(&constraint.body, &negation)?;
4474            Ok(constraint)
4475        })
4476        .collect::<Result<Vec<_>>>()?;
4477    if let WfsNegationTransform::Rename {
4478        names,
4479        source_schemas,
4480    } = negation
4481    {
4482        add_wfs_fixed_predicates(&mut out, names, source_schemas)?;
4483    }
4484    Ok(out)
4485}
4486
4487fn transform_wfs_body(
4488    body: &[BodyLiteral],
4489    negation: &WfsNegationTransform<'_>,
4490) -> Result<Vec<BodyLiteral>> {
4491    let mut transformed = Vec::with_capacity(body.len());
4492    for literal in body {
4493        match (literal, negation) {
4494            (BodyLiteral::Negated(_), WfsNegationTransform::Drop) => {}
4495            (BodyLiteral::Negated(atom), WfsNegationTransform::Rename { names, .. }) => {
4496                let mut atom = atom.clone();
4497                atom.predicate = names.get(&atom.predicate).cloned().ok_or_else(|| {
4498                    XlogError::Execution(format!(
4499                        "missing WFS fixed relation name for {}",
4500                        atom.predicate
4501                    ))
4502                })?;
4503                if atom.terms.is_empty() {
4504                    atom.terms.push(Term::Integer(1));
4505                }
4506                transformed.push(BodyLiteral::Negated(atom));
4507            }
4508            _ => transformed.push(literal.clone()),
4509        }
4510    }
4511    Ok(transformed)
4512}
4513
4514fn add_wfs_fixed_predicates(
4515    program: &mut Program,
4516    names: &HashMap<String, String>,
4517    source_schemas: &HashMap<String, Schema>,
4518) -> Result<()> {
4519    let existing: BTreeSet<String> = program
4520        .predicates
4521        .iter()
4522        .map(|decl| decl.name.clone())
4523        .collect();
4524    for (source, fixed) in names {
4525        if existing.contains(fixed) {
4526            return Err(XlogError::UnsupportedEpistemicConstruct {
4527                construct: "GPU WFS fixed relation name".to_string(),
4528                context: format!(
4529                    "internal fixed relation {fixed} collides with a declared predicate"
4530                ),
4531            });
4532        }
4533        let Some(schema) = source_schemas.get(source) else {
4534            return Err(XlogError::UnsupportedEpistemicConstruct {
4535                construct: "GPU WFS fixed relation schema".to_string(),
4536                context: format!(
4537                    "ordinary schema inference produced no schema for negated predicate {source}"
4538                ),
4539            });
4540        };
4541
4542        let is_private = program
4543            .predicates
4544            .iter()
4545            .find(|declaration| declaration.name == *source)
4546            .is_some_and(|declaration| declaration.is_private);
4547        let columns = if schema.arity() == 0 {
4548            vec![PredColumn {
4549                name: Some("present".to_string()),
4550                typ: TypeRef::Scalar(ScalarType::U32),
4551            }]
4552        } else {
4553            schema
4554                .columns
4555                .iter()
4556                .map(|(name, scalar_type)| PredColumn {
4557                    name: Some(name.clone()),
4558                    typ: TypeRef::Scalar(*scalar_type),
4559                })
4560                .collect::<Vec<_>>()
4561        };
4562        program.predicates.push(PredDecl {
4563            name: fixed.clone(),
4564            types: columns.iter().map(|column| column.typ.clone()).collect(),
4565            columns,
4566            is_private,
4567        });
4568    }
4569    Ok(())
4570}
4571
4572fn wfs_negated_predicates(program: &Program) -> BTreeSet<String> {
4573    program
4574        .rules
4575        .iter()
4576        .map(|rule| &rule.body)
4577        .chain(
4578            program
4579                .constraints
4580                .iter()
4581                .map(|constraint| &constraint.body),
4582        )
4583        .flatten()
4584        .filter_map(|lit| match lit {
4585            BodyLiteral::Negated(atom) => Some(atom.predicate.clone()),
4586            _ => None,
4587        })
4588        .collect()
4589}
4590
4591fn wfs_intensional_predicates(program: &Program) -> Vec<String> {
4592    program
4593        .proper_rules()
4594        .map(|rule| rule.head.predicate.clone())
4595        .collect::<BTreeSet<_>>()
4596        .into_iter()
4597        .collect()
4598}
4599
4600fn wfs_fixed_names(
4601    program: &Program,
4602    predicates: &BTreeSet<String>,
4603    prefix: &str,
4604) -> HashMap<String, String> {
4605    let mut reserved: BTreeSet<String> = program
4606        .predicates
4607        .iter()
4608        .map(|decl| decl.name.clone())
4609        .collect();
4610    let mut names = HashMap::new();
4611    for pred in predicates {
4612        let mut candidate = format!("{prefix}_{pred}");
4613        if reserved.contains(&candidate) {
4614            let mut suffix = 0usize;
4615            loop {
4616                let suffixed = format!("{prefix}_{suffix}_{pred}");
4617                if !reserved.contains(&suffixed) {
4618                    candidate = suffixed;
4619                    break;
4620                }
4621                suffix += 1;
4622            }
4623        }
4624        reserved.insert(candidate.clone());
4625        names.insert(pred.clone(), candidate);
4626    }
4627    names
4628}
4629
4630fn wfs_plan_combined_schemas(plan: &EpistemicWfsGpuPlan) -> HashMap<String, Schema> {
4631    let mut schemas = HashMap::new();
4632    for ordinary in [&plan.overapprox, &plan.lower, &plan.upper] {
4633        for (name, schema) in &ordinary.schemas {
4634            schemas
4635                .entry(name.clone())
4636                .or_insert_with(|| schema.clone());
4637        }
4638    }
4639    schemas
4640}
4641
4642fn g91_plan_combined_schemas(plan: &EpistemicG91CompatibilityGpuPlan) -> HashMap<String, Schema> {
4643    let mut schemas = HashMap::new();
4644    for pass in [&plan.upper_bound, &plan.refinement] {
4645        for (name, schema) in gpu_evaluation_pass_schemas(pass) {
4646            schemas.entry(name).or_insert(schema);
4647        }
4648    }
4649    schemas
4650}
4651
4652fn gpu_evaluation_pass_schemas(pass: &GpuEvaluationPass) -> HashMap<String, Schema> {
4653    match pass {
4654        GpuEvaluationPass::Ordinary(ordinary) => ordinary.schemas.clone(),
4655        GpuEvaluationPass::Wfs(wfs) => wfs_plan_combined_schemas(wfs),
4656    }
4657}
4658
4659fn g91_plan_combined_rel_ids(plan: &EpistemicG91CompatibilityGpuPlan) -> HashMap<String, RelId> {
4660    let mut rel_ids = HashMap::new();
4661    for pass in [&plan.upper_bound, &plan.refinement] {
4662        for (name, rel_id) in gpu_evaluation_pass_rel_ids(pass) {
4663            rel_ids.insert(name, rel_id);
4664        }
4665    }
4666    rel_ids
4667}
4668
4669fn gpu_evaluation_pass_rel_ids(pass: &GpuEvaluationPass) -> HashMap<String, RelId> {
4670    match pass {
4671        GpuEvaluationPass::Ordinary(ordinary) => ordinary.rel_ids.clone(),
4672        GpuEvaluationPass::Wfs(wfs) => wfs_plan_combined_rel_ids(wfs),
4673    }
4674}
4675
4676fn wfs_plan_combined_rel_ids(plan: &EpistemicWfsGpuPlan) -> HashMap<String, RelId> {
4677    let mut rel_ids = HashMap::new();
4678    for ordinary in [&plan.overapprox, &plan.lower, &plan.upper] {
4679        for (name, rel_id) in &ordinary.rel_ids {
4680            rel_ids.insert(name.clone(), *rel_id);
4681        }
4682    }
4683    rel_ids
4684}
4685
4686fn schema_from_pred_decl(
4687    decl: &xlog_logic::ast::PredDecl,
4688    domains: &HashMap<String, ScalarType>,
4689) -> Result<Schema> {
4690    let columns = decl.schema_columns();
4691    let resolved = columns
4692        .iter()
4693        .enumerate()
4694        .map(|(idx, column)| {
4695            let name = column.name.clone().unwrap_or_else(|| format!("c{idx}"));
4696            resolve_pred_column_type(&decl.name, idx, &column.typ, domains).map(|typ| (name, typ))
4697        })
4698        .collect::<Result<Vec<_>>>()?;
4699    Ok(Schema::new(resolved))
4700}
4701
4702fn resolve_pred_column_type(
4703    predicate: &str,
4704    index: usize,
4705    typ: &TypeRef,
4706    domains: &HashMap<String, ScalarType>,
4707) -> Result<ScalarType> {
4708    match typ {
4709        TypeRef::Scalar(ty) => Ok(*ty),
4710        TypeRef::Domain(name) => domains.get(name).copied().ok_or_else(|| {
4711            XlogError::Compilation(format!(
4712                "unknown domain alias '{}' in predicate '{}' column {}",
4713                name, predicate, index
4714            ))
4715        }),
4716        TypeRef::List(_) | TypeRef::Term | TypeRef::Compound | TypeRef::PredRef => {
4717            Ok(ScalarType::U64)
4718        }
4719    }
4720}
4721
4722fn schema_from_terms(terms: &[Term]) -> Schema {
4723    let columns = terms
4724        .iter()
4725        .enumerate()
4726        .map(|(idx, term)| (format!("c{idx}"), term.inferred_scalar_type()))
4727        .collect();
4728    Schema::new(columns)
4729}
4730
4731/// Desugar a shared-variable epistemic constraint — a constraint with at least one
4732/// epistemic literal and a variable appearing in more than one term position across the body
4733/// (the join `:- know p(X), possible q(X).`, the diagonal `:- know p(X, X).`, or the
4734/// negated-difference `:- q(X), not know p(X).`) — into an ordinary extraction rule plus a
4735/// single-occurrence modal over it:
4736///
4737/// ```text
4738///   :- BodyLit1, BodyLit2, ..., BodyLitN.
4739///        ==> __epi_join_N(Vars) :- ord(BodyLit1), ..., ord(BodyLitN).
4740///            :- know __epi_join_N(Vars).
4741/// ```
4742///
4743/// where `ord` ordinary-izes each modal literal (`know/possible r(..)` -> `r(..)`,
4744/// `not know/possible r(..)` -> `not r(..)`) and keeps non-modal literals unchanged. For a
4745/// base/EDB or purely-ordinary-derived modal target `know r == possible r == r`, so the
4746/// ordinary join `__epi_join_N` is exactly the set of variable bindings the constraint
4747/// forbids; the single-occurrence `:- know __epi_join_N(Vars)` then routes through the
4748/// existing variable-keyed world-view constraint path, which prunes the world view to empty —
4749/// with NO new kernel. Applied at the normalization choke point so BOTH the reduced ordinary
4750/// materialization and the epistemic planner observe the helper relation (an EIR-only rewrite
4751/// is accepted at planning but the helper is never materialized).
4752///
4753/// Guarded to non-modal-derived targets (where the `know == possible == ordinary`
4754/// equivalence holds); a constraint with a modal-derived target is left unchanged and falls
4755/// through to the core compiler's existing shared-variable rejection. Single-occurrence
4756/// variable-keyed modal, distinct-variable multi-literal, and ground constraints have no
4757/// repeated variable and are likewise untouched.
4758fn desugar_shared_variable_epistemic_constraints(mut program: Program) -> Program {
4759    // A predicate defined by any rule carrying an epistemic body literal is "modal-derived":
4760    // for it `know p`/`possible p` is NOT equal to the ordinary `p`, so ordinary-izing it
4761    // would be UNSOUND. Restrict the desugaring to base/EDB or purely-ordinary-derived
4762    // targets (where `know p == possible p == p`), the case for base tuple-key targets.
4763    let modal_derived: BTreeSet<String> = program
4764        .rules
4765        .iter()
4766        .filter(|rule| {
4767            rule.body
4768                .iter()
4769                .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
4770        })
4771        .map(|rule| rule.head.predicate.clone())
4772        .collect();
4773    let mut extraction_rules: Vec<Rule> = Vec::new();
4774    let mut counter = 0usize;
4775    for constraint in &mut program.constraints {
4776        let has_epistemic = constraint
4777            .body
4778            .iter()
4779            .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
4780        if !has_epistemic || !constraint_has_shared_variable(&constraint.body) {
4781            continue;
4782        }
4783        // Sound only when EVERY modal target is non-modal-derived (know == possible == ord).
4784        let has_modal_derived_target = constraint.body.iter().any(|lit| {
4785            matches!(lit, BodyLiteral::Epistemic(e) if modal_derived.contains(&e.atom.predicate))
4786        });
4787        if has_modal_derived_target {
4788            continue;
4789        }
4790        let distinct = distinct_body_variables(&constraint.body);
4791        let helper = format!("__epi_join_{counter}");
4792        counter += 1;
4793        let helper_terms: Vec<Term> = distinct.iter().map(|v| Term::Variable(v.clone())).collect();
4794        let helper_body: Vec<BodyLiteral> = constraint
4795            .body
4796            .iter()
4797            .map(ordinaryize_modal_literal)
4798            .collect();
4799        extraction_rules.push(Rule {
4800            head: Atom {
4801                predicate: helper.clone(),
4802                terms: helper_terms.clone(),
4803            },
4804            body: helper_body,
4805        });
4806        // Replace the whole constraint with a single-occurrence modal over the join helper.
4807        constraint.body = vec![BodyLiteral::Epistemic(EpistemicLiteral {
4808            op: EpistemicOp::Know,
4809            negated: false,
4810            atom: Atom {
4811                predicate: helper,
4812                terms: helper_terms,
4813            },
4814        })];
4815    }
4816    program.rules.extend(extraction_rules);
4817    program
4818}
4819
4820/// Replace a modal literal with its ordinary counterpart (`know/possible r` -> `r`,
4821/// `not know/possible r` -> `not r`); non-modal literals are returned unchanged. Sound for
4822/// the shared-variable constraint desugaring when the modal target is non-modal-derived,
4823/// where `know r == possible r == r`.
4824fn ordinaryize_modal_literal(lit: &BodyLiteral) -> BodyLiteral {
4825    match lit {
4826        BodyLiteral::Epistemic(e) if e.negated => BodyLiteral::Negated(e.atom.clone()),
4827        BodyLiteral::Epistemic(e) => BodyLiteral::Positive(e.atom.clone()),
4828        other => other.clone(),
4829    }
4830}
4831
4832/// True if some variable occurs in more than one atom term position across the constraint
4833/// body — the signature of a join / diagonal / negated-difference the core compiler rejects.
4834fn constraint_has_shared_variable(body: &[BodyLiteral]) -> bool {
4835    let mut counts: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
4836    for lit in body {
4837        if let Some(atom) = lit.atom() {
4838            for term in &atom.terms {
4839                if let Term::Variable(name) = term {
4840                    *counts.entry(name.clone()).or_insert(0) += 1;
4841                }
4842            }
4843        }
4844    }
4845    counts.values().any(|&count| count > 1)
4846}
4847
4848/// Ordered DISTINCT variable names appearing in atom positions across the constraint body
4849/// (first-appearance order), used as the extracted helper relation's columns.
4850fn distinct_body_variables(body: &[BodyLiteral]) -> Vec<String> {
4851    let mut seen = BTreeSet::new();
4852    let mut order = Vec::new();
4853    for lit in body {
4854        if let Some(atom) = lit.atom() {
4855            for term in &atom.terms {
4856                if let Term::Variable(name) = term {
4857                    if seen.insert(name.clone()) {
4858                        order.push(name.clone());
4859                    }
4860                }
4861            }
4862        }
4863    }
4864    order
4865}
4866
4867fn augment_same_name_multi_arity_schemas(
4868    program: &Program,
4869    schemas: &mut HashMap<String, Schema>,
4870) -> Result<()> {
4871    let predicates = if program_has_epistemic_literals(program) {
4872        epistemic_extensional_multi_arity_predicates(program)
4873    } else {
4874        predicate_arities(program)
4875            .into_iter()
4876            .filter_map(|(predicate, arities)| (arities.len() > 1).then_some(predicate))
4877            .collect()
4878    };
4879    let domains: HashMap<String, ScalarType> = program
4880        .domains
4881        .iter()
4882        .map(|domain| (domain.name.clone(), domain.typ))
4883        .collect();
4884
4885    for decl in &program.predicates {
4886        if !predicates.contains(&decl.name) {
4887            continue;
4888        }
4889        let key = arity_qualified_name(&decl.name, decl.arity());
4890        schemas.insert(key, schema_from_pred_decl(decl, &domains)?);
4891    }
4892
4893    for fact in program.facts() {
4894        let pred = fact.head.predicate.as_str();
4895        let arity = fact.head.terms.len();
4896        if !predicates.contains(pred) {
4897            continue;
4898        }
4899        let key = arity_qualified_name(pred, arity);
4900        schemas
4901            .entry(key)
4902            .or_insert_with(|| schema_from_terms(&fact.head.terms));
4903    }
4904
4905    for rule in &program.rules {
4906        augment_atom_schema_if_needed(&rule.head, &predicates, schemas);
4907        for literal in &rule.body {
4908            match literal {
4909                BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
4910                    augment_atom_schema_if_needed(atom, &predicates, schemas);
4911                }
4912                BodyLiteral::Epistemic(epistemic) => {
4913                    augment_atom_schema_if_needed(&epistemic.atom, &predicates, schemas);
4914                }
4915                BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
4916            }
4917        }
4918    }
4919
4920    for query in &program.queries {
4921        augment_atom_schema_if_needed(&query.atom, &predicates, schemas);
4922    }
4923
4924    Ok(())
4925}
4926
4927/// Arity-qualify same-name multi-arity predicates; `None` when the program has
4928/// no such overloads (the caller then compiles `program` itself, unchanged).
4929fn qualify_same_name_multi_arity_program(program: &Program) -> Option<Program> {
4930    let overloaded = predicate_arities(program)
4931        .into_iter()
4932        .filter_map(|(predicate, arities)| (arities.len() > 1).then_some(predicate))
4933        .collect::<BTreeSet<_>>();
4934    if overloaded.is_empty() {
4935        return None;
4936    }
4937
4938    let mut qualified = program.clone();
4939    for declaration in &mut qualified.predicates {
4940        if overloaded.contains(&declaration.name) {
4941            declaration.name = arity_qualified_name(&declaration.name, declaration.arity());
4942        }
4943    }
4944    for rule in &mut qualified.rules {
4945        qualify_atom_arity(&mut rule.head, &overloaded);
4946        qualify_body_literal_arities(&mut rule.body, &overloaded);
4947    }
4948    for constraint in &mut qualified.constraints {
4949        qualify_body_literal_arities(&mut constraint.body, &overloaded);
4950    }
4951    for query in &mut qualified.queries {
4952        qualify_atom_arity(&mut query.atom, &overloaded);
4953    }
4954    Some(qualified)
4955}
4956
4957fn qualify_body_literal_arities(literals: &mut [BodyLiteral], overloaded: &BTreeSet<String>) {
4958    for literal in literals {
4959        match literal {
4960            BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
4961                qualify_atom_arity(atom, overloaded);
4962            }
4963            BodyLiteral::Epistemic(epistemic) => {
4964                qualify_atom_arity(&mut epistemic.atom, overloaded);
4965            }
4966            BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
4967        }
4968    }
4969}
4970
4971fn qualify_atom_arity(atom: &mut Atom, overloaded: &BTreeSet<String>) {
4972    if overloaded.contains(&atom.predicate) {
4973        atom.predicate = arity_qualified_name(&atom.predicate, atom.terms.len());
4974    }
4975}
4976
4977fn augment_atom_schema_if_needed(
4978    atom: &Atom,
4979    predicates: &BTreeSet<String>,
4980    schemas: &mut HashMap<String, Schema>,
4981) {
4982    if !predicates.contains(&atom.predicate) {
4983        return;
4984    }
4985    let key = arity_qualified_name(&atom.predicate, atom.terms.len());
4986    schemas
4987        .entry(key)
4988        .or_insert_with(|| schema_from_terms(&atom.terms));
4989}
4990
4991fn predicate_arities(program: &Program) -> HashMap<String, BTreeSet<usize>> {
4992    let mut arities = HashMap::new();
4993    for decl in &program.predicates {
4994        add_predicate_arity(&mut arities, &decl.name, decl.arity());
4995    }
4996    for rule in &program.rules {
4997        add_predicate_arity(&mut arities, &rule.head.predicate, rule.head.terms.len());
4998        for literal in &rule.body {
4999            match literal {
5000                BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
5001                    add_predicate_arity(&mut arities, &atom.predicate, atom.terms.len());
5002                }
5003                BodyLiteral::Epistemic(epistemic) => {
5004                    add_predicate_arity(
5005                        &mut arities,
5006                        &epistemic.atom.predicate,
5007                        epistemic.atom.terms.len(),
5008                    );
5009                }
5010                BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
5011            }
5012        }
5013    }
5014    for query in &program.queries {
5015        add_predicate_arity(&mut arities, &query.atom.predicate, query.atom.terms.len());
5016    }
5017    for constraint in &program.constraints {
5018        for literal in &constraint.body {
5019            match literal {
5020                BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
5021                    add_predicate_arity(&mut arities, &atom.predicate, atom.terms.len());
5022                }
5023                BodyLiteral::Epistemic(epistemic) => {
5024                    add_predicate_arity(
5025                        &mut arities,
5026                        &epistemic.atom.predicate,
5027                        epistemic.atom.terms.len(),
5028                    );
5029                }
5030                BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
5031            }
5032        }
5033    }
5034    arities
5035}
5036
5037fn add_predicate_arity(
5038    arities: &mut HashMap<String, BTreeSet<usize>>,
5039    predicate: &str,
5040    arity: usize,
5041) {
5042    arities
5043        .entry(predicate.to_string())
5044        .or_default()
5045        .insert(arity);
5046}
5047
5048fn arity_qualified_name(predicate: &str, arity: usize) -> String {
5049    format!("{predicate}/{arity}")
5050}
5051
5052fn program_has_epistemic_literals(program: &Program) -> bool {
5053    program.rules.iter().any(|rule| {
5054        rule.body
5055            .iter()
5056            .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
5057    }) || program.constraints.iter().any(|constraint| {
5058        constraint
5059            .body
5060            .iter()
5061            .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
5062    })
5063}
5064
5065fn epistemic_output_head_predicate_count(program: &Program) -> usize {
5066    program
5067        .rules
5068        .iter()
5069        .filter(|rule| {
5070            rule.body
5071                .iter()
5072                .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
5073        })
5074        .map(|rule| rule.head.predicate.as_str())
5075        .collect::<BTreeSet<_>>()
5076        .len()
5077}
5078
5079/// The user-visible output head predicate(s) of a stratum's epistemic-bearing
5080/// rules. For a recursive stratum (`reach :- reach, know a`) this is the recursive
5081/// head whose materialized relation is the stratum's output.
5082fn epistemic_stratum_output_heads(program: &Program) -> Vec<String> {
5083    program
5084        .rules
5085        .iter()
5086        .filter(|rule| {
5087            rule.body
5088                .iter()
5089                .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
5090        })
5091        .map(|rule| rule.head.predicate.clone())
5092        .collect::<BTreeSet<_>>()
5093        .into_iter()
5094        .collect()
5095}
5096
5097fn epistemic_relation_ids(plan: &LogicExecutionPlan) -> Result<HashMap<String, RelId>> {
5098    let mut rel_ids = HashMap::new();
5099    match plan {
5100        LogicExecutionPlan::EpistemicSingle(executable) => {
5101            for (name, rel_id) in &executable.relation_ids {
5102                insert_epistemic_relation_id(&mut rel_ids, name, *rel_id)?;
5103            }
5104        }
5105        LogicExecutionPlan::EpistemicSplit(split) => {
5106            for component in &split.components {
5107                for (name, rel_id) in &component.executable.relation_ids {
5108                    insert_epistemic_relation_id(&mut rel_ids, name, *rel_id)?;
5109                }
5110            }
5111        }
5112        LogicExecutionPlan::EpistemicStratified(stratified) => {
5113            for stratum in &stratified.strata {
5114                match &stratum.plan {
5115                    StratumPlanKind::Single(executable) => {
5116                        for (name, rel_id) in &executable.relation_ids {
5117                            // Each stratum is a distinct sub-program compiled with a
5118                            // fresh compiler, so relation ids legitimately differ
5119                            // across strata; keep the last writer per name.
5120                            rel_ids.insert(name.clone(), *rel_id);
5121                        }
5122                    }
5123                    StratumPlanKind::Split(split) => {
5124                        for component in &split.components {
5125                            for (name, rel_id) in &component.executable.relation_ids {
5126                                rel_ids.insert(name.clone(), *rel_id);
5127                            }
5128                        }
5129                    }
5130                    // An ordinary (Case-A recursive) stratum carries no epistemic
5131                    // relation-id map; its relations are owned by its own ordinary
5132                    // RIR plan and surfaced from the store after execution.
5133                    StratumPlanKind::Ordinary { .. } => {}
5134                }
5135            }
5136        }
5137        LogicExecutionPlan::EpistemicWfsGpu(wfs) => {
5138            for plan in [&wfs.overapprox, &wfs.lower, &wfs.upper] {
5139                for (name, rel_id) in &plan.rel_ids {
5140                    rel_ids.insert(name.clone(), *rel_id);
5141                }
5142            }
5143        }
5144        LogicExecutionPlan::EpistemicG91Compatibility(g91) => {
5145            for pass in [&g91.upper_bound, &g91.refinement] {
5146                for (name, rel_id) in gpu_evaluation_pass_rel_ids(pass) {
5147                    rel_ids.insert(name, rel_id);
5148                }
5149            }
5150        }
5151        LogicExecutionPlan::Ordinary(_) => {}
5152    }
5153    Ok(rel_ids)
5154}
5155
5156fn insert_epistemic_relation_id(
5157    rel_ids: &mut HashMap<String, RelId>,
5158    name: &str,
5159    rel_id: RelId,
5160) -> Result<()> {
5161    if let Some(previous) = rel_ids.insert(name.to_string(), rel_id) {
5162        if previous != rel_id {
5163            return Err(XlogError::Compilation(format!(
5164                "epistemic split components assigned conflicting relation ids for {name}: \
5165                 {previous:?} vs {rel_id:?}"
5166            )));
5167        }
5168    }
5169    Ok(())
5170}
5171
5172fn capacities_for_epistemic_executable(
5173    executable: &EpistemicExecutablePlan,
5174) -> Result<EpistemicGpuWorkspaceCapacities> {
5175    let literal_count = executable.gpu_plan.epistemic_literals.len();
5176    let max_candidates = 1usize.checked_shl(literal_count as u32).ok_or_else(|| {
5177        XlogError::UnsupportedEpistemicConstruct {
5178            construct: "epistemic GPU execution candidate generation".to_string(),
5179            context: format!("literal count {literal_count} exceeds target pointer width"),
5180        }
5181    })?;
5182    Ok(EpistemicGpuWorkspaceCapacities {
5183        max_candidates,
5184        max_worlds: 1,
5185        max_models_per_reduction: DEFAULT_EPISTEMIC_MAX_MODELS_PER_REDUCTION,
5186    })
5187}
5188
5189fn capacities_for_epistemic_split(
5190    split: &EpistemicSplitExecutablePlan,
5191) -> Result<EpistemicGpuWorkspaceCapacities> {
5192    let mut capacities = EpistemicGpuWorkspaceCapacities {
5193        max_candidates: 1,
5194        max_worlds: 1,
5195        max_models_per_reduction: DEFAULT_EPISTEMIC_MAX_MODELS_PER_REDUCTION,
5196    };
5197    for component in &split.components {
5198        let component_capacities = capacities_for_epistemic_executable(&component.executable)?;
5199        capacities.max_candidates = capacities
5200            .max_candidates
5201            .max(component_capacities.max_candidates);
5202    }
5203    Ok(capacities)
5204}
5205
5206fn epistemic_output_relation_name(executable: &EpistemicExecutablePlan) -> Result<String> {
5207    executable
5208        .gpu_plan
5209        .reductions
5210        .last()
5211        .map(|reduction| reduction.head_predicate.clone())
5212        .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
5213            construct: "epistemic GPU reduced output".to_string(),
5214            context: "executable plan has no epistemic reductions".to_string(),
5215        })
5216}
5217
5218fn epistemic_buffer_to_query_result(relation_name: String, buffer: CudaBuffer) -> LogicQueryResult {
5219    let schema = buffer.schema();
5220    let columns = schema
5221        .columns
5222        .iter()
5223        .map(|(name, _)| name.clone())
5224        .collect();
5225    let sort_labels = schema.sort_labels().to_vec();
5226    LogicQueryResult {
5227        relation_name,
5228        columns,
5229        sort_labels,
5230        buffer,
5231    }
5232}
5233
5234/// Convert an epistemic GPU execution result into one query result per output head.
5235///
5236/// `primary_relation_name` is the primary head (from `final_output`). A
5237/// JOINT-SOLVED coalesced multi-head component also carries
5238/// `additional_head_outputs`, each materialized against the SAME accepted world
5239/// view; every coupled head becomes its own query result so `xlog run` displays
5240/// all coupled epistemic outputs.
5241fn epistemic_result_to_query_results(
5242    primary_relation_name: String,
5243    result: EpistemicGpuExecutionResult,
5244) -> Vec<LogicQueryResult> {
5245    let mut results = Vec::with_capacity(1 + result.additional_head_outputs.len());
5246    for (head, buffer) in result.additional_head_outputs {
5247        results.push(epistemic_buffer_to_query_result(head, buffer));
5248    }
5249    results.push(epistemic_buffer_to_query_result(
5250        primary_relation_name,
5251        result.final_output,
5252    ));
5253    results
5254}
5255
5256fn is_user_visible_relation(name: &str) -> bool {
5257    !name.starts_with("__")
5258}
5259
5260fn is_list_helper_relation(name: &str) -> bool {
5261    name.starts_with("__xlog_list_")
5262}
5263
5264fn relation_clone_error(context: String, error: XlogError) -> XlogError {
5265    match error {
5266        XlogError::ResourceExhausted {
5267            context: source_context,
5268            estimated_bytes,
5269            budget_bytes,
5270        } => XlogError::ResourceExhausted {
5271            context: format!("{context}: {source_context}"),
5272            estimated_bytes,
5273            budget_bytes,
5274        },
5275        XlogError::Kernel(message) => XlogError::Kernel(format!("{context}: {message}")),
5276        error => error,
5277    }
5278}
5279
5280fn logic_delta_report(
5281    stats: DeltaRecomputeStats,
5282    insert_rows: u64,
5283    delete_rows: u64,
5284) -> LogicDeltaReport {
5285    LogicDeltaReport {
5286        input_delta_count: stats.changed_relations,
5287        changed_relations: stats.changed_relations,
5288        changed_relation_names: Vec::new(),
5289        insert_rows,
5290        delete_rows,
5291        has_deletes: stats.has_deletes,
5292        affected_sccs: stats.affected_sccs,
5293        recomputed_sccs: stats.recomputed_sccs,
5294        incremental_sccs: stats.incremental_sccs,
5295        coalesced_insert_rows: insert_rows,
5296        coalesced_delete_rows: delete_rows,
5297        canceled_rows: 0,
5298        planner_telemetry: DeltaPlannerTelemetry::default(),
5299        debug_trace: Vec::new(),
5300    }
5301}
5302
5303fn no_op_delta_report(seed: PreparedRelationDeltaReportSeed) -> LogicDeltaReport {
5304    LogicDeltaReport {
5305        input_delta_count: seed.input_delta_count,
5306        changed_relations: 0,
5307        changed_relation_names: Vec::new(),
5308        insert_rows: 0,
5309        delete_rows: 0,
5310        has_deletes: false,
5311        affected_sccs: 0,
5312        recomputed_sccs: 0,
5313        incremental_sccs: 0,
5314        coalesced_insert_rows: 0,
5315        coalesced_delete_rows: 0,
5316        canceled_rows: seed.canceled_rows,
5317        planner_telemetry: DeltaPlannerTelemetry {
5318            fallback_decision: "no_op".to_string(),
5319            ..DeltaPlannerTelemetry::default()
5320        },
5321        debug_trace: vec![format!("canceled_rows={}", seed.canceled_rows)],
5322    }
5323}
5324
5325fn delta_debug_trace(report: &LogicDeltaReport) -> Vec<String> {
5326    vec![
5327        format!("changed_relation_names={:?}", report.changed_relation_names),
5328        format!("affected_sccs={}", report.affected_sccs),
5329        format!("recomputed_sccs={}", report.recomputed_sccs),
5330        format!("incremental_sccs={}", report.incremental_sccs),
5331        format!("insert_rows={}", report.insert_rows),
5332        format!("delete_rows={}", report.delete_rows),
5333        format!(
5334            "planner_fallback_decision={}",
5335            report.planner_telemetry.fallback_decision
5336        ),
5337        format!(
5338            "estimated_delta_speedup={:?}",
5339            report.planner_telemetry.estimated_delta_speedup
5340        ),
5341    ]
5342}
5343
5344fn buffers_gpu_set_equivalent(
5345    provider: &CudaKernelProvider,
5346    left: &CudaBuffer,
5347    right: &CudaBuffer,
5348) -> Result<bool> {
5349    if left.schema() != right.schema() {
5350        return Ok(false);
5351    }
5352    let left_rows = provider.device_row_count(left)?;
5353    let right_rows = provider.device_row_count(right)?;
5354    if left_rows != right_rows {
5355        return Ok(false);
5356    }
5357
5358    let left_minus_right = provider.diff_full_row(left, right)?;
5359    if provider.device_row_count(&left_minus_right)? != 0 {
5360        return Ok(false);
5361    }
5362    let right_minus_left = provider.diff_full_row(right, left)?;
5363    Ok(provider.device_row_count(&right_minus_left)? == 0)
5364}
5365
5366fn coalesce_relation_delta_batch_with_cancellation_capture(
5367    provider: &CudaKernelProvider,
5368    delta_batch: Vec<(String, RelationDelta)>,
5369    cancellation_capture_relations: &BTreeSet<String>,
5370) -> Result<PreparedRelationDeltaBatch> {
5371    let input_delta_count = delta_batch.len();
5372    let mut pending_by_relation: HashMap<String, PendingRelationDelta> = HashMap::new();
5373    let mut cancellations: HashMap<String, Vec<RelationDeltaCancellation>> = HashMap::new();
5374    let mut canceled_rows = 0u64;
5375
5376    for (update_index, (name, delta)) in delta_batch.into_iter().enumerate() {
5377        let capture_cancellations = cancellation_capture_relations.contains(&name);
5378        let cancellation_relation = capture_cancellations.then(|| name.clone());
5379        let mut update_cancellations = capture_cancellations.then(Vec::new);
5380        let pending = pending_by_relation.entry(name).or_default();
5381        if let Some(insert) = delta.insert {
5382            merge_insert_delta(
5383                provider,
5384                pending,
5385                insert,
5386                &mut canceled_rows,
5387                update_index,
5388                update_cancellations.as_mut(),
5389            )?;
5390        }
5391        if let Some(delete) = delta.delete {
5392            merge_delete_delta(
5393                provider,
5394                pending,
5395                delete,
5396                &mut canceled_rows,
5397                update_index,
5398                update_cancellations.as_mut(),
5399            )?;
5400        }
5401        if let Some(mut captured) = update_cancellations.filter(|trace| !trace.is_empty()) {
5402            cancellations
5403                .entry(cancellation_relation.expect("capture relation must be retained"))
5404                .or_default()
5405                .append(&mut captured);
5406        }
5407    }
5408
5409    let mut deltas = HashMap::new();
5410    let mut coalesced_insert_rows = 0u64;
5411    let mut coalesced_delete_rows = 0u64;
5412    for (name, pending) in pending_by_relation {
5413        let insert = pending.insert.and_then(non_empty_buffer);
5414        let delete = pending.delete.and_then(non_empty_buffer);
5415        if insert.is_none() && delete.is_none() {
5416            continue;
5417        }
5418        coalesced_insert_rows += insert.as_ref().map(buffer_rows).unwrap_or(0);
5419        coalesced_delete_rows += delete.as_ref().map(buffer_rows).unwrap_or(0);
5420        deltas.insert(name, RelationDelta::new(insert, delete));
5421    }
5422
5423    let changed_relations = deltas.len();
5424    Ok(PreparedRelationDeltaBatch {
5425        deltas,
5426        cancellations,
5427        report_seed: PreparedRelationDeltaReportSeed {
5428            input_delta_count,
5429            changed_relations,
5430            coalesced_insert_rows,
5431            coalesced_delete_rows,
5432            canceled_rows,
5433        },
5434    })
5435}
5436
5437fn merge_insert_delta(
5438    provider: &CudaKernelProvider,
5439    pending: &mut PendingRelationDelta,
5440    insert: CudaBuffer,
5441    canceled_rows: &mut u64,
5442    update_index: usize,
5443    cancellations: Option<&mut Vec<RelationDeltaCancellation>>,
5444) -> Result<()> {
5445    let mut incoming = provider.dedup_full_row(&insert)?;
5446    if let Some(delete) = pending.delete.take().and_then(non_empty_buffer) {
5447        let delete_before = buffer_rows(&delete);
5448        let delete_after = provider.diff_full_row(&delete, &incoming)?;
5449        let insert_after = provider.diff_full_row(&incoming, &delete)?;
5450        *canceled_rows += delete_before.saturating_sub(buffer_rows(&delete_after));
5451        capture_canceled_tuples(
5452            provider,
5453            &incoming,
5454            &insert_after,
5455            update_index,
5456            RelationDeltaDirection::Insert,
5457            cancellations,
5458        )?;
5459        pending.delete = non_empty_buffer(delete_after);
5460        incoming = insert_after;
5461    }
5462    pending.insert = merge_optional_buffer(provider, pending.insert.take(), incoming)?;
5463    Ok(())
5464}
5465
5466fn merge_delete_delta(
5467    provider: &CudaKernelProvider,
5468    pending: &mut PendingRelationDelta,
5469    delete: CudaBuffer,
5470    canceled_rows: &mut u64,
5471    update_index: usize,
5472    cancellations: Option<&mut Vec<RelationDeltaCancellation>>,
5473) -> Result<()> {
5474    let mut incoming = provider.dedup_full_row(&delete)?;
5475    if let Some(insert) = pending.insert.take().and_then(non_empty_buffer) {
5476        let insert_before = buffer_rows(&insert);
5477        let insert_after = provider.diff_full_row(&insert, &incoming)?;
5478        let delete_after = provider.diff_full_row(&incoming, &insert)?;
5479        *canceled_rows += insert_before.saturating_sub(buffer_rows(&insert_after));
5480        capture_canceled_tuples(
5481            provider,
5482            &incoming,
5483            &delete_after,
5484            update_index,
5485            RelationDeltaDirection::Delete,
5486            cancellations,
5487        )?;
5488        pending.insert = non_empty_buffer(insert_after);
5489        incoming = delete_after;
5490    }
5491    pending.delete = merge_optional_buffer(provider, pending.delete.take(), incoming)?;
5492    Ok(())
5493}
5494
5495fn capture_canceled_tuples(
5496    provider: &CudaKernelProvider,
5497    incoming: &CudaBuffer,
5498    incoming_after_cancellation: &CudaBuffer,
5499    update_index: usize,
5500    incoming_direction: RelationDeltaDirection,
5501    cancellations: Option<&mut Vec<RelationDeltaCancellation>>,
5502) -> Result<()> {
5503    let Some(cancellations) = cancellations else {
5504        return Ok(());
5505    };
5506    let intersection = provider.diff_full_row(incoming, incoming_after_cancellation)?;
5507    if let Some(tuples) = non_empty_buffer(intersection) {
5508        cancellations.push(RelationDeltaCancellation {
5509            update_index,
5510            incoming_direction,
5511            tuples,
5512        });
5513    }
5514    Ok(())
5515}
5516
5517fn merge_optional_buffer(
5518    provider: &CudaKernelProvider,
5519    existing: Option<CudaBuffer>,
5520    incoming: CudaBuffer,
5521) -> Result<Option<CudaBuffer>> {
5522    let Some(incoming) = non_empty_buffer(incoming) else {
5523        return Ok(existing.and_then(non_empty_buffer));
5524    };
5525    match existing.and_then(non_empty_buffer) {
5526        Some(existing) => provider
5527            .union_gpu(&existing, &incoming)
5528            .map(non_empty_buffer),
5529        None => Ok(Some(incoming)),
5530    }
5531}
5532
5533fn non_empty_buffer(buffer: CudaBuffer) -> Option<CudaBuffer> {
5534    if buffer.cached_row_count() == Some(0) || buffer.is_empty() {
5535        None
5536    } else {
5537        Some(buffer)
5538    }
5539}
5540
5541fn buffer_rows(buffer: &CudaBuffer) -> u64 {
5542    buffer
5543        .cached_row_count()
5544        .map(u64::from)
5545        .unwrap_or_else(|| buffer.num_rows())
5546}
5547
5548fn ensure_schema_type_compatible(expected: &Schema, actual: &Schema) -> Result<()> {
5549    if expected.arity() != actual.arity() {
5550        return Err(XlogError::Execution(format!(
5551            "Expected {} columns, got {}",
5552            expected.arity(),
5553            actual.arity()
5554        )));
5555    }
5556    for i in 0..expected.arity() {
5557        let exp = expected.column_type(i).ok_or_else(|| {
5558            XlogError::Execution(format!("Missing expected type for column {}", i))
5559        })?;
5560        let act = actual
5561            .column_type(i)
5562            .ok_or_else(|| XlogError::Execution(format!("Missing actual type for column {}", i)))?;
5563        if exp != act {
5564            return Err(XlogError::Execution(format!(
5565                "Column {} type mismatch: expected {:?}, got {:?}",
5566                i, exp, act
5567            )));
5568        }
5569    }
5570    Ok(())
5571}
5572
5573fn query_output_vars(Query { atom }: &Query) -> Vec<String> {
5574    let mut out = Vec::new();
5575    let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
5576    for term in &atom.terms {
5577        for name in term.variables() {
5578            if seen.insert(name) {
5579                out.push(name.to_string());
5580            }
5581        }
5582    }
5583    out
5584}
5585
5586// --------------------------------------------------------------------------- //
5587// Epistemic-plan / EIR JSON dump
5588// --------------------------------------------------------------------------- //
5589
5590fn json_escape(s: &str) -> String {
5591    let mut out = String::with_capacity(s.len() + 2);
5592    for c in s.chars() {
5593        match c {
5594            '"' => out.push_str("\\\""),
5595            '\\' => out.push_str("\\\\"),
5596            '\n' => out.push_str("\\n"),
5597            '\r' => out.push_str("\\r"),
5598            '\t' => out.push_str("\\t"),
5599            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
5600            c => out.push(c),
5601        }
5602    }
5603    out
5604}
5605
5606/// Deterministic 64-bit FNV-1a hash of a string (stable across runs/processes,
5607/// unlike `std::hash::DefaultHasher` which is randomized). Used as the stable
5608/// epistemic plan id so two dumps of the same plan compare equal.
5609fn fnv1a_64(s: &str) -> u64 {
5610    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
5611    for b in s.as_bytes() {
5612        hash ^= *b as u64;
5613        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
5614    }
5615    hash
5616}
5617
5618/// Extract every `know`/`possible` literal (with negation) from a program's EIR.
5619/// Used to retain epistemic provenance when a Case-A recursive reduction lowers the
5620/// program to an ordinary executable plan.
5621fn collect_eir_epistemic_literals(program: &Program) -> Vec<xlog_ir::EirEpistemicLiteral> {
5622    let mut lits = Vec::new();
5623    if let Ok(eir) = xlog_logic::build_eir(program) {
5624        for rule in &eir.rules {
5625            for lit in &rule.body {
5626                if let xlog_ir::EirBodyLiteral::Epistemic(e) = lit {
5627                    lits.push(e.clone());
5628                }
5629            }
5630        }
5631    }
5632    lits
5633}
5634
5635/// JSON summary for an epistemic source that reduced to a high-level recursive
5636/// execution plan without single-pass epistemic GPU candidate units. Case-A/B
5637/// stratified reductions use the ordinary semi-naive engine; cyclic negated-modal
5638/// reductions use the GPU-backed WFS alternating-fixpoint plan. In both cases the
5639/// modal literals and the fail-closed GPU execution policy are recorded.
5640fn epistemic_provenance_summary_json(
5641    plan_kind: &str,
5642    prov: &EpistemicProvenance,
5643    max_iterations: Option<usize>,
5644    wfs: Option<&EpistemicWfsGpuPlan>,
5645) -> String {
5646    let literals = prov
5647        .literals
5648        .iter()
5649        .map(epistemic_literal_json)
5650        .collect::<Vec<_>>()
5651        .join(",");
5652    let wfs_fixed_relations = wfs
5653        .map(wfs_fixed_relations_json)
5654        .unwrap_or_else(|| "null".to_string());
5655    let wfs_convergence_predicates = wfs
5656        .map(wfs_convergence_predicates_json)
5657        .unwrap_or_else(|| "null".to_string());
5658    let wfs_gpu_passes = if wfs.is_some() {
5659        "[\"overapprox\",\"lower\",\"upper\"]"
5660    } else {
5661        "null"
5662    };
5663    let body = format!(
5664        "{{\"plan_kind\":\"{}\",\"reduction\":\"{}\",\
5665\"epistemic_literals\":[{}],\"units\":[],\"max_iterations\":{},\
5666\"wfs_fixed_relations\":{},\"wfs_convergence_predicates\":{},\
5667\"wfs_gpu_passes\":{},\"execution_backend\":\"{}\",\
5668\"fallback_policy\":\"{}\"}}",
5669        json_escape(plan_kind),
5670        json_escape(prov.reduction),
5671        literals,
5672        max_iterations
5673            .map(|value| value.to_string())
5674            .unwrap_or_else(|| "null".to_string()),
5675        wfs_fixed_relations,
5676        wfs_convergence_predicates,
5677        wfs_gpu_passes,
5678        epistemic_execution_backend_json(xlog_ir::EpistemicExecutionBackend::Gpu),
5679        epistemic_fallback_policy_json(xlog_ir::EpistemicFallbackPolicy::RejectUnsupported)
5680    );
5681    let plan_id = fnv1a_64(&body);
5682    format!(
5683        "{{\"plan_id\":\"epi-{:016x}\",\"plan_kind\":\"{}\",\
5684\"reduction\":\"{}\",\"epistemic_literals\":[{}],\"units\":[],\
5685\"max_iterations\":{},\"wfs_fixed_relations\":{},\
5686\"wfs_convergence_predicates\":{},\"wfs_gpu_passes\":{},\"execution_backend\":\"{}\",\
5687\"fallback_policy\":\"{}\"}}",
5688        plan_id,
5689        json_escape(plan_kind),
5690        json_escape(prov.reduction),
5691        literals,
5692        max_iterations
5693            .map(|value| value.to_string())
5694            .unwrap_or_else(|| "null".to_string()),
5695        wfs_fixed_relations,
5696        wfs_convergence_predicates,
5697        wfs_gpu_passes,
5698        epistemic_execution_backend_json(xlog_ir::EpistemicExecutionBackend::Gpu),
5699        epistemic_fallback_policy_json(xlog_ir::EpistemicFallbackPolicy::RejectUnsupported)
5700    )
5701}
5702
5703fn g91_compatibility_summary_json(
5704    plan_kind: &str,
5705    provenance: &EpistemicProvenance,
5706    plan: &EpistemicG91CompatibilityGpuPlan,
5707) -> String {
5708    let literals = provenance
5709        .literals
5710        .iter()
5711        .map(epistemic_literal_json)
5712        .collect::<Vec<_>>()
5713        .join(",");
5714    let snapshots = plan
5715        .snapshot_relations
5716        .iter()
5717        .map(|(source, snapshot)| {
5718            format!("\"{}\":\"{}\"", json_escape(source), json_escape(snapshot))
5719        })
5720        .collect::<Vec<_>>()
5721        .join(",");
5722    let convergence = plan
5723        .convergence_predicates
5724        .iter()
5725        .map(|predicate| format!("\"{}\"", json_escape(predicate)))
5726        .collect::<Vec<_>>()
5727        .join(",");
5728    let body = format!(
5729        "{{\"plan_kind\":\"{}\",\"reduction\":\"{}\",\
5730\"epistemic_literals\":[{}],\"units\":[],\"max_iterations\":{},\
5731\"snapshot_relations\":{{{}}},\"convergence_predicates\":[{}],\
5732\"gpu_passes\":[\"upper_bound\",\"refinement\"],\
5733\"execution_backend\":\"{}\",\"fallback_policy\":\"{}\"}}",
5734        json_escape(plan_kind),
5735        json_escape(provenance.reduction),
5736        literals,
5737        plan.max_iterations,
5738        snapshots,
5739        convergence,
5740        epistemic_execution_backend_json(xlog_ir::EpistemicExecutionBackend::Gpu),
5741        epistemic_fallback_policy_json(xlog_ir::EpistemicFallbackPolicy::RejectUnsupported)
5742    );
5743    let plan_id = fnv1a_64(&body);
5744    format!(
5745        "{{\"plan_id\":\"epi-{plan_id:016x}\",\"plan_kind\":\"{}\",\
5746\"reduction\":\"{}\",\"epistemic_literals\":[{}],\"units\":[],\
5747\"max_iterations\":{},\"snapshot_relations\":{{{}}},\
5748\"convergence_predicates\":[{}],\
5749\"gpu_passes\":[\"upper_bound\",\"refinement\"],\
5750\"execution_backend\":\"{}\",\"fallback_policy\":\"{}\"}}",
5751        json_escape(plan_kind),
5752        json_escape(provenance.reduction),
5753        literals,
5754        plan.max_iterations,
5755        snapshots,
5756        convergence,
5757        epistemic_execution_backend_json(xlog_ir::EpistemicExecutionBackend::Gpu),
5758        epistemic_fallback_policy_json(xlog_ir::EpistemicFallbackPolicy::RejectUnsupported)
5759    )
5760}
5761
5762fn wfs_fixed_relations_json(wfs: &EpistemicWfsGpuPlan) -> String {
5763    let mut sources: BTreeSet<&str> = BTreeSet::new();
5764    for source in wfs.upper_fixed_names.keys() {
5765        sources.insert(source.as_str());
5766    }
5767    for source in wfs.lower_fixed_names.keys() {
5768        sources.insert(source.as_str());
5769    }
5770    let entries = sources
5771        .into_iter()
5772        .map(|source| {
5773            let upper = wfs
5774                .upper_fixed_names
5775                .get(source)
5776                .map(String::as_str)
5777                .unwrap_or("");
5778            let lower = wfs
5779                .lower_fixed_names
5780                .get(source)
5781                .map(String::as_str)
5782                .unwrap_or("");
5783            format!(
5784                "\"{}\":{{\"upper\":\"{}\",\"lower\":\"{}\"}}",
5785                json_escape(source),
5786                json_escape(upper),
5787                json_escape(lower)
5788            )
5789        })
5790        .collect::<Vec<_>>()
5791        .join(",");
5792    format!("{{{entries}}}")
5793}
5794
5795fn wfs_convergence_predicates_json(wfs: &EpistemicWfsGpuPlan) -> String {
5796    let entries = wfs
5797        .intensional_predicates
5798        .iter()
5799        .map(|pred| format!("\"{}\"", json_escape(pred)))
5800        .collect::<Vec<_>>()
5801        .join(",");
5802    format!("[{entries}]")
5803}
5804
5805fn epistemic_literal_json(lit: &xlog_ir::EirEpistemicLiteral) -> String {
5806    let op = match lit.op {
5807        xlog_ir::EirEpistemicOp::Know => "know",
5808        xlog_ir::EirEpistemicOp::Possible => "possible",
5809    };
5810    format!(
5811        "{{\"op\":\"{}\",\"negated\":{},\"predicate\":\"{}\",\"arity\":{}}}",
5812        op,
5813        lit.negated,
5814        json_escape(&lit.atom.predicate),
5815        lit.atom.arity
5816    )
5817}
5818
5819fn epistemic_execution_backend_json(backend: xlog_ir::EpistemicExecutionBackend) -> &'static str {
5820    match backend {
5821        xlog_ir::EpistemicExecutionBackend::Gpu => "gpu",
5822    }
5823}
5824
5825fn epistemic_fallback_policy_json(policy: xlog_ir::EpistemicFallbackPolicy) -> &'static str {
5826    match policy {
5827        xlog_ir::EpistemicFallbackPolicy::RejectUnsupported => "reject_unsupported",
5828    }
5829}
5830
5831fn epistemic_gpu_plan_json(plan: &xlog_ir::EpistemicGpuPlan) -> String {
5832    let mode = match plan.mode {
5833        xlog_ir::EirEpistemicMode::G91 => "g91",
5834        xlog_ir::EirEpistemicMode::Faeel => "faeel",
5835    };
5836    let literals = plan
5837        .epistemic_literals
5838        .iter()
5839        .map(epistemic_literal_json)
5840        .collect::<Vec<_>>()
5841        .join(",");
5842    let phases = plan
5843        .required_phases
5844        .iter()
5845        .map(|p| format!("\"{:?}\"", p))
5846        .collect::<Vec<_>>()
5847        .join(",");
5848    let kernels = plan
5849        .required_kernel_phases
5850        .iter()
5851        .map(|p| format!("\"{:?}\"", p))
5852        .collect::<Vec<_>>()
5853        .join(",");
5854    let constraints = plan
5855        .constraints
5856        .iter()
5857        .map(|c| {
5858            let idx = c
5859                .literal_indices
5860                .iter()
5861                .map(|i| i.to_string())
5862                .collect::<Vec<_>>()
5863                .join(",");
5864            format!(
5865                "{{\"constraint_index\":{},\"literal_indices\":[{}]}}",
5866                c.constraint_index, idx
5867            )
5868        })
5869        .collect::<Vec<_>>()
5870        .join(",");
5871    let reductions = plan
5872        .reductions
5873        .iter()
5874        .map(|r| {
5875            format!(
5876                "{{\"rule_index\":{},\"head\":\"{}\",\"public_head_arity\":{},\"relational_body_atoms\":{}}}",
5877                r.rule_index,
5878                json_escape(&r.head_predicate),
5879                r.public_head_arity,
5880                r.relational_body_atoms
5881            )
5882        })
5883        .collect::<Vec<_>>()
5884        .join(",");
5885    format!(
5886        "{{\"mode\":\"{}\",\"epistemic_literals\":[{}],\"required_phases\":[{}],\
5887\"required_kernel_phases\":[{}],\"constraints\":[{}],\"reductions\":[{}],\
5888\"execution_backend\":\"{}\",\"fallback_policy\":\"{}\"}}",
5889        mode,
5890        literals,
5891        phases,
5892        kernels,
5893        constraints,
5894        reductions,
5895        epistemic_execution_backend_json(plan.execution_backend),
5896        epistemic_fallback_policy_json(plan.fallback_policy)
5897    )
5898}
5899
5900fn epistemic_plan_summary_json(
5901    plan_kind: &str,
5902    gpu_plans: &[(String, &xlog_ir::EpistemicGpuPlan)],
5903    has_ordinary_post: bool,
5904) -> String {
5905    let mut units = gpu_plans
5906        .iter()
5907        .map(|(label, plan)| {
5908            format!(
5909                "{{\"unit\":\"{}\",\"plan\":{}}}",
5910                json_escape(label),
5911                epistemic_gpu_plan_json(plan)
5912            )
5913        })
5914        .collect::<Vec<_>>();
5915    if has_ordinary_post {
5916        units.push(
5917            "{\"unit\":\"ordinary_post\",\"stage_kind\":\"ordinary_closure_and_constraints\"}"
5918                .to_string(),
5919        );
5920    }
5921    let units = units.join(",");
5922    // Canonical body (without the id) hashed for the stable plan id.
5923    let body = format!(
5924        "{{\"plan_kind\":\"{}\",\"units\":[{}],\"execution_backend\":\"{}\",\"fallback_policy\":\"{}\"}}",
5925        json_escape(plan_kind),
5926        units,
5927        epistemic_execution_backend_json(xlog_ir::EpistemicExecutionBackend::Gpu),
5928        epistemic_fallback_policy_json(xlog_ir::EpistemicFallbackPolicy::RejectUnsupported)
5929    );
5930    let plan_id = fnv1a_64(&body);
5931    format!(
5932        "{{\"plan_id\":\"epi-{:016x}\",\"plan_kind\":\"{}\",\"units\":[{}],\"execution_backend\":\"{}\",\"fallback_policy\":\"{}\"}}",
5933        plan_id,
5934        json_escape(plan_kind),
5935        units,
5936        epistemic_execution_backend_json(xlog_ir::EpistemicExecutionBackend::Gpu),
5937        epistemic_fallback_policy_json(xlog_ir::EpistemicFallbackPolicy::RejectUnsupported)
5938    )
5939}
5940
5941#[cfg(test)]
5942fn finish_test_provider_setup<T>(provider: Result<T>, require_cuda: bool) -> Option<T> {
5943    match provider {
5944        Ok(provider) => Some(provider),
5945        Err(error) if require_cuda => {
5946            panic!("XLOG_REQUIRE_CUDA=1 but CUDA provider construction failed: {error}")
5947        }
5948        Err(error) => {
5949            eprintln!("Skipping test: no CUDA device available ({error})");
5950            None
5951        }
5952    }
5953}
5954
5955#[cfg(test)]
5956mod tests {
5957    use super::*;
5958    use std::sync::Arc;
5959
5960    use xlog_core::{symbol, MemoryBudget, ScalarType};
5961    use xlog_cuda::{cuda_graph::CudaGraphNodeKind, CudaDevice, GpuMemoryManager};
5962    use xlog_ir::RirNode;
5963    use xlog_runtime::resident_graph::{
5964        ResidentGraphDeclineReason, ResidentGraphRouteCertificate, ResidentGraphSchemaCatalog,
5965        ResidentGraphSelectionKind,
5966    };
5967
5968    fn ground_term_encoding_test_provider() -> Option<Arc<CudaKernelProvider>> {
5969        let provider = (|| -> Result<Arc<CudaKernelProvider>> {
5970            let device = Arc::new(CudaDevice::new(0)?);
5971            let memory = Arc::new(GpuMemoryManager::new(
5972                device.clone(),
5973                MemoryBudget::with_limit(256 * 1024 * 1024),
5974            ));
5975            Ok(Arc::new(CudaKernelProvider::new(device, memory)?))
5976        })();
5977
5978        finish_test_provider_setup(
5979            provider,
5980            std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1"),
5981        )
5982    }
5983
5984    fn pinned_corpus_test_provider() -> Option<Arc<CudaKernelProvider>> {
5985        let provider = (|| -> Result<Arc<CudaKernelProvider>> {
5986            let device = Arc::new(CudaDevice::new(0)?);
5987            let memory = Arc::new(GpuMemoryManager::new(
5988                device.clone(),
5989                MemoryBudget::with_limit(2 * 1024 * 1024 * 1024),
5990            ));
5991            Ok(Arc::new(CudaKernelProvider::new(device, memory)?))
5992        })();
5993        finish_test_provider_setup(
5994            provider,
5995            std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1"),
5996        )
5997    }
5998
5999    const PINNED_CORPUS_SHA: &str = "74f2895486737b4caa42229389d309994e7ad3ea";
6000    const RESIDENT_ENV_NAMES: [&str; 4] = [
6001        "XLOG_DISABLE_RESIDENT_RECURSION",
6002        "XLOG_USE_RESIDENT_RECURSION",
6003        "XLOG_REQUIRE_RESIDENT_RECURSION",
6004        RESIDENT_LATENCY_DIAGNOSTICS_ENV,
6005    ];
6006
6007    fn git_output(corpus: &std::path::Path, args: &[&str]) -> String {
6008        let output = std::process::Command::new("git")
6009            .arg("-C")
6010            .arg(corpus)
6011            .args(args)
6012            .output()
6013            .unwrap_or_else(|error| panic!("git {args:?} failed to start: {error}"));
6014        assert!(
6015            output.status.success(),
6016            "git {args:?} failed: {}",
6017            String::from_utf8_lossy(&output.stderr)
6018        );
6019        String::from_utf8(output.stdout).expect("git output must be UTF-8")
6020    }
6021
6022    fn assert_exact_clean_corpus(corpus: &std::path::Path) {
6023        assert_eq!(
6024            git_output(corpus, &["rev-parse", "HEAD"]).trim(),
6025            PINNED_CORPUS_SHA
6026        );
6027        assert!(
6028            git_output(
6029                corpus,
6030                &["status", "--porcelain=v1", "--untracked-files=all"]
6031            )
6032            .is_empty(),
6033            "pinned corpus must have no tracked or untracked modifications"
6034        );
6035        assert!(
6036            git_output(corpus, &["diff", "--no-ext-diff", "--submodule=diff"]).is_empty(),
6037            "pinned corpus working tree must match HEAD"
6038        );
6039        assert!(
6040            git_output(
6041                corpus,
6042                &["diff", "--cached", "--no-ext-diff", "--submodule=diff"]
6043            )
6044            .is_empty(),
6045            "pinned corpus index must match HEAD"
6046        );
6047        let submodules = git_output(corpus, &["submodule", "status", "--recursive"]);
6048        assert!(
6049            submodules.lines().all(|line| line.starts_with(' ')),
6050            "every recursive submodule must be initialized at its recorded commit: {submodules}"
6051        );
6052        let clean_submodules = std::process::Command::new("git")
6053            .arg("-C")
6054            .arg(corpus)
6055            .args([
6056                "submodule",
6057                "foreach",
6058                "--quiet",
6059                "--recursive",
6060                "test -z \"$(git status --porcelain=v1 --untracked-files=all)\"",
6061            ])
6062            .status()
6063            .expect("recursive submodule cleanliness command must start");
6064        assert!(
6065            clean_submodules.success(),
6066            "recursive submodule checkout is dirty"
6067        );
6068    }
6069
6070    struct ResidentEnvGuard {
6071        old: Vec<(&'static str, Option<std::ffi::OsString>)>,
6072    }
6073
6074    impl ResidentEnvGuard {
6075        fn set(active: &[(&'static str, &'static str)]) -> Self {
6076            let old = RESIDENT_ENV_NAMES
6077                .into_iter()
6078                .map(|name| (name, std::env::var_os(name)))
6079                .collect();
6080            for name in RESIDENT_ENV_NAMES {
6081                // SAFETY: every test using this helper holds `resident_env_lock`.
6082                unsafe { std::env::remove_var(name) };
6083            }
6084            for (name, value) in active {
6085                // SAFETY: every test using this helper holds `resident_env_lock`.
6086                unsafe { std::env::set_var(name, value) };
6087            }
6088            Self { old }
6089        }
6090    }
6091
6092    impl Drop for ResidentEnvGuard {
6093        fn drop(&mut self) {
6094            for (name, value) in self.old.drain(..) {
6095                match value {
6096                    Some(value) => {
6097                        // SAFETY: every test using this helper holds `resident_env_lock`.
6098                        unsafe { std::env::set_var(name, value) };
6099                    }
6100                    None => {
6101                        // SAFETY: every test using this helper holds `resident_env_lock`.
6102                        unsafe { std::env::remove_var(name) };
6103                    }
6104                }
6105            }
6106        }
6107    }
6108
6109    fn resident_env_lock() -> &'static std::sync::Mutex<()> {
6110        static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
6111        LOCK.get_or_init(|| std::sync::Mutex::new(()))
6112    }
6113
6114    fn corpus_program(corpus: &std::path::Path) -> Result<LogicProgram> {
6115        let entry = corpus.join("scenarios/acceptance/issue1/q01_blind.xlog");
6116        let source = std::fs::read_to_string(&entry).map_err(|error| {
6117            XlogError::Execution(format!("failed to read {}: {error}", entry.display()))
6118        })?;
6119        let resolver = xlog_logic::compile::load_modules(&entry, vec![corpus.join("programs")])
6120            .map_err(|error| XlogError::Compilation(error.to_string()))?;
6121        LogicProgram::compile_with_resolver(&source, &resolver)
6122    }
6123
6124    fn schema_catalog(program: &LogicProgram) -> ResidentGraphSchemaCatalog {
6125        ResidentGraphSchemaCatalog::from_named_schemas(program.rel_ids.iter().filter_map(
6126            |(name, rel)| {
6127                program
6128                    .schemas
6129                    .get(name)
6130                    .cloned()
6131                    .map(|schema| (name.clone(), *rel, schema))
6132            },
6133        ))
6134    }
6135
6136    fn scan_schema_descriptor(program: &LogicProgram, rel: RelId) -> String {
6137        let schemas = program
6138            .rel_ids
6139            .iter()
6140            .filter(|(_, candidate)| **candidate == rel)
6141            .filter_map(|(name, _)| {
6142                program
6143                    .schemas
6144                    .get(name)
6145                    .map(|schema| format!("{name}={schema:#?}"))
6146            })
6147            .collect::<BTreeSet<_>>();
6148        assert!(
6149            !schemas.is_empty(),
6150            "compiled Scan {rel:?} has no schema identity"
6151        );
6152        schemas.into_iter().collect::<Vec<_>>().join("|")
6153    }
6154
6155    struct RouteWalk<'a> {
6156        program: &'a LogicProgram,
6157        scc_index: usize,
6158        rule_index: usize,
6159        recursive: bool,
6160        descriptors: &'a mut BTreeSet<String>,
6161    }
6162
6163    impl RouteWalk<'_> {
6164        fn visit(&mut self, node: &RirNode, path: &str) {
6165            let scan_schema = match node {
6166                RirNode::Scan { rel } => scan_schema_descriptor(self.program, *rel),
6167                _ => String::new(),
6168            };
6169            assert!(
6170                self.descriptors.insert(format!(
6171                    "scc={};rule={};recursive={};path={path};node={node:#?};scan_schema={scan_schema}",
6172                    self.scc_index, self.rule_index, self.recursive
6173                )),
6174                "route occurrence paths must be unique"
6175            );
6176            match node {
6177                RirNode::Unit | RirNode::Scan { .. } | RirNode::TensorMaskedJoin { .. } => {}
6178                RirNode::Filter { input, .. }
6179                | RirNode::Project { input, .. }
6180                | RirNode::GroupBy { input, .. }
6181                | RirNode::Distinct { input, .. } => self.visit(input, &format!("{path}/input")),
6182                RirNode::Join { left, right, .. } | RirNode::Diff { left, right } => {
6183                    self.visit(left, &format!("{path}/left"));
6184                    self.visit(right, &format!("{path}/right"));
6185                }
6186                RirNode::ChainJoin {
6187                    left,
6188                    right,
6189                    fallback,
6190                    ..
6191                } => {
6192                    self.visit(left, &format!("{path}/primary/left"));
6193                    self.visit(right, &format!("{path}/primary/right"));
6194                    self.visit(fallback, &format!("{path}/alternative/captured_fallback"));
6195                }
6196                RirNode::Union { inputs } => {
6197                    for (index, input) in inputs.iter().enumerate() {
6198                        self.visit(input, &format!("{path}/input[{index}]"));
6199                    }
6200                }
6201                RirNode::Fixpoint {
6202                    base, recursive, ..
6203                } => {
6204                    self.visit(base, &format!("{path}/base"));
6205                    self.visit(recursive, &format!("{path}/recursive"));
6206                }
6207                RirNode::MultiWayJoin {
6208                    inputs, fallback, ..
6209                } => {
6210                    for (index, input) in inputs.iter().enumerate() {
6211                        self.visit(input, &format!("{path}/primary/input[{index}]"));
6212                    }
6213                    self.visit(fallback, &format!("{path}/alternative/captured_fallback"));
6214                }
6215            }
6216        }
6217    }
6218
6219    fn independent_route_descriptors(
6220        program: &LogicProgram,
6221        plan: &ExecutionPlan,
6222    ) -> BTreeSet<String> {
6223        let mut descriptors = BTreeSet::new();
6224        for (scc_index, scc) in plan.sccs.iter().enumerate() {
6225            let rules = plan
6226                .rules_by_scc
6227                .get(scc_index)
6228                .unwrap_or_else(|| panic!("missing rule vector for SCC {scc_index}"));
6229            for (rule_index, rule) in rules.iter().enumerate() {
6230                RouteWalk {
6231                    program,
6232                    scc_index,
6233                    rule_index,
6234                    recursive: scc.is_recursive,
6235                    descriptors: &mut descriptors,
6236                }
6237                .visit(&rule.body, "primary/root");
6238                let rule_identity = format!(
6239                    "scc={scc_index};rule={rule_index};head={};schema={:#?}",
6240                    rule.head, rule.meta.schema
6241                );
6242                descriptors.insert(format!("{rule_identity};implicit=rule_result_union"));
6243                descriptors.insert(format!("{rule_identity};implicit=full_row_dedup"));
6244                if scc.is_recursive {
6245                    descriptors.insert(format!("{rule_identity};implicit=novel_tuple_difference"));
6246                    descriptors.insert(format!("{rule_identity};implicit=device_convergence"));
6247                }
6248            }
6249        }
6250        descriptors
6251    }
6252
6253    fn op_count(stats: &ExecutionStats, name: &str) -> usize {
6254        stats
6255            .strata
6256            .iter()
6257            .flat_map(|stratum| &stratum.ops)
6258            .filter(|op| op.op_name == name)
6259            .count()
6260    }
6261
6262    fn strata_op_profile(stats: &ExecutionStats) -> BTreeMap<String, (usize, u64, u64)> {
6263        let mut profile = BTreeMap::new();
6264        for op in stats.strata.iter().flat_map(|stratum| &stratum.ops) {
6265            let entry = profile.entry(op.op_name.clone()).or_insert((0, 0, 0));
6266            entry.0 += 1;
6267            entry.1 += op.input_rows;
6268            entry.2 += op.output_rows;
6269        }
6270        profile
6271    }
6272
6273    #[derive(Debug, Clone, PartialEq, Eq)]
6274    struct HostQuerySnapshot {
6275        relation_name: String,
6276        columns: Vec<String>,
6277        sort_labels: Vec<String>,
6278        schema: Schema,
6279        rows: Vec<Vec<u64>>,
6280    }
6281
6282    fn snapshot_query_results(
6283        provider: &CudaKernelProvider,
6284        result: &LogicEvalResult,
6285    ) -> Result<Vec<HostQuerySnapshot>> {
6286        result
6287            .queries
6288            .iter()
6289            .map(|query| {
6290                let row_count = usize::try_from(provider.device_row_count(&query.buffer)?)
6291                    .map_err(|_| XlogError::Execution("query row count exceeds usize".into()))?;
6292                let mut columns = Vec::with_capacity(query.buffer.schema().arity());
6293                for index in 0..query.buffer.schema().arity() {
6294                    let ty = query
6295                        .buffer
6296                        .schema()
6297                        .column_type(index)
6298                        .expect("schema arity checked");
6299                    let values = match ty {
6300                        ScalarType::U32 | ScalarType::Symbol => provider
6301                            .download_column::<u32>(&query.buffer, index)?
6302                            .into_iter()
6303                            .map(u64::from)
6304                            .collect(),
6305                        ScalarType::U64 => provider.download_column::<u64>(&query.buffer, index)?,
6306                        ScalarType::I32 => provider
6307                            .download_column::<i32>(&query.buffer, index)?
6308                            .into_iter()
6309                            .map(|value| value as i64 as u64)
6310                            .collect(),
6311                        ScalarType::I64 => provider
6312                            .download_column::<i64>(&query.buffer, index)?
6313                            .into_iter()
6314                            .map(|value| value as u64)
6315                            .collect(),
6316                        ScalarType::F32 => provider
6317                            .download_column::<f32>(&query.buffer, index)?
6318                            .into_iter()
6319                            .map(|value| u64::from(value.to_bits()))
6320                            .collect(),
6321                        ScalarType::F64 => provider
6322                            .download_column::<f64>(&query.buffer, index)?
6323                            .into_iter()
6324                            .map(f64::to_bits)
6325                            .collect(),
6326                        ScalarType::Bool => provider
6327                            .download_column::<u8>(&query.buffer, index)?
6328                            .into_iter()
6329                            .map(u64::from)
6330                            .collect(),
6331                    };
6332                    if values.len() != row_count {
6333                        return Err(XlogError::Execution(format!(
6334                            "query column {index} has {} rows but metadata reports {row_count}",
6335                            values.len()
6336                        )));
6337                    }
6338                    columns.push(values);
6339                }
6340                let mut rows = (0..row_count)
6341                    .map(|row| columns.iter().map(|column| column[row]).collect::<Vec<_>>())
6342                    .collect::<Vec<_>>();
6343                rows.sort_unstable();
6344                Ok(HostQuerySnapshot {
6345                    relation_name: query.relation_name.clone(),
6346                    columns: query.columns.clone(),
6347                    sort_labels: query.sort_labels.clone(),
6348                    schema: query.buffer.schema().clone(),
6349                    rows,
6350                })
6351            })
6352            .collect()
6353    }
6354
6355    #[test]
6356    #[ignore = "requires a serialized release-mode CUDA acceptance run"]
6357    fn resident_semantic_profile_excludes_noop_recursive_variants() -> Result<()> {
6358        let _env_lock = resident_env_lock().lock().expect("resident env lock");
6359        let Some(provider) = ground_term_encoding_test_provider() else {
6360            return Ok(());
6361        };
6362        let program = LogicProgram::compile(
6363            r#"
6364                pred seed(u32).
6365                pred dead(u32).
6366                pred a(u32).
6367                pred b(u32).
6368
6369                seed(1).
6370                a(X) :- seed(X).
6371                b(X) :- dead(X).
6372                a(X) :- b(X), X = 1.
6373                b(X) :- a(X), X = 1.
6374
6375                ?- a(X).
6376                ?- b(X).
6377            "#,
6378        )?;
6379        let empty_recursive_inputs = || -> Result<HashMap<String, CudaBuffer>> {
6380            Ok(HashMap::from([
6381                (
6382                    "a".to_string(),
6383                    provider.create_empty_buffer(program.schema("a").expect("a schema").clone())?,
6384                ),
6385                (
6386                    "b".to_string(),
6387                    provider.create_empty_buffer(program.schema("b").expect("b schema").clone())?,
6388                ),
6389            ]))
6390        };
6391        let baseline = {
6392            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
6393            program.evaluate_with_options(provider.clone(), empty_recursive_inputs()?, true)?
6394        };
6395        let expected = snapshot_query_results(provider.as_ref(), &baseline)?;
6396        let baseline_stats = baseline.stats.as_ref().expect("baseline profile");
6397        let baseline_scans = op_count(baseline_stats, "scan");
6398        let baseline_filters = op_count(baseline_stats, "filter");
6399        let expected_semantic_scans =
6400            baseline_scans as u64 + baseline_stats.chain_fallback_scan_equivalents;
6401        let expected_semantic_filters =
6402            baseline_filters as u64 + baseline_stats.chain_fallback_filter_equivalents;
6403        drop(baseline);
6404
6405        let resident = {
6406            let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
6407            program.evaluate_with_options(provider.clone(), empty_recursive_inputs()?, true)?
6408        };
6409        assert_eq!(
6410            snapshot_query_results(provider.as_ref(), &resident)?,
6411            expected
6412        );
6413        let resident_stats = resident.stats.as_ref().expect("resident profile");
6414        let graph = resident_stats
6415            .resident_graph
6416            .as_ref()
6417            .expect("resident telemetry");
6418        assert_eq!(graph.semantic_scan_invocations, expected_semantic_scans);
6419        assert_eq!(graph.semantic_filter_invocations, expected_semantic_filters);
6420        assert_eq!(
6421            op_count(resident_stats, "scan") as u64,
6422            graph.device_scan_invocations
6423        );
6424        assert_eq!(
6425            op_count(resident_stats, "filter") as u64,
6426            graph.device_filter_invocations
6427        );
6428        assert!(graph.device_scan_invocations >= graph.semantic_scan_invocations);
6429        assert!(graph.device_filter_invocations >= graph.semantic_filter_invocations);
6430        assert!(
6431            graph.device_scan_invocations > graph.semantic_scan_invocations
6432                || graph.device_filter_invocations > graph.semantic_filter_invocations,
6433            "the witness must schedule at least one empty-delta recursive variant"
6434        );
6435        Ok(())
6436    }
6437
6438    #[test]
6439    fn resident_latency_phase_accounting_reports_only_unmeasured_host_work() {
6440        assert_eq!(resident_latency_unattributed_ns(100, &[10, 20, 30]), 40);
6441        assert_eq!(resident_latency_unattributed_ns(50, &[30, 30]), 0);
6442    }
6443
6444    #[test]
6445    fn resident_certification_cache_is_eager_clone_shared_thread_safe_and_compile_isolated(
6446    ) -> Result<()> {
6447        let source = r#"
6448            pred input(u32).
6449            pred output(u32).
6450            output(X) :- input(X).
6451            ?- output(X).
6452        "#;
6453        let program = LogicProgram::compile(source)?;
6454        assert_eq!(program.resident_certification_initializations(), 1);
6455
6456        let barrier = Arc::new(std::sync::Barrier::new(8));
6457        let certified = std::thread::scope(|scope| {
6458            let handles = (0..8)
6459                .map(|_| {
6460                    let clone = program.clone();
6461                    let barrier = Arc::clone(&barrier);
6462                    scope.spawn(move || {
6463                        barrier.wait();
6464                        clone.resident_certified_plan()
6465                    })
6466                })
6467                .collect::<Vec<_>>();
6468            handles
6469                .into_iter()
6470                .map(|handle| handle.join().expect("resident certification worker"))
6471                .collect::<Result<Vec<_>>>()
6472        })?;
6473        assert_eq!(program.resident_certification_initializations(), 1);
6474        assert!(certified
6475            .iter()
6476            .all(|candidate| Arc::ptr_eq(&certified[0], candidate)));
6477
6478        let outcome_program = LogicProgram::compile(source)?;
6479        assert_eq!(outcome_program.resident_certification_initializations(), 1);
6480        let (seeded, cache_was_warm, initialized_here) =
6481            outcome_program.resident_certified_plan_with_outcome()?;
6482        assert!(cache_was_warm);
6483        assert!(!initialized_here);
6484        let (warm, cache_was_warm, initialized_here) =
6485            outcome_program.resident_certified_plan_with_outcome()?;
6486        assert!(cache_was_warm);
6487        assert!(!initialized_here);
6488        assert!(Arc::ptr_eq(&seeded, &warm));
6489
6490        let fresh = LogicProgram::compile(source)?;
6491        assert_eq!(fresh.resident_certification_initializations(), 1);
6492        let fresh_certified = fresh.resident_certified_plan()?;
6493        assert_eq!(fresh.resident_certification_initializations(), 1);
6494        assert!(!Arc::ptr_eq(&certified[0], &fresh_certified));
6495        Ok(())
6496    }
6497
6498    #[test]
6499    fn resident_certification_retains_only_query_and_constraint_dependencies() -> Result<()> {
6500        let program = LogicProgram::compile(
6501            r#"
6502                pred base(u32).
6503                pred edge(u32, u32).
6504                pred reachable(u32).
6505                pred audited(u32).
6506                pred disconnected_seed(u32).
6507                pred disconnected(u32).
6508
6509                base(1).
6510                edge(1, 2).
6511                reachable(X) :- base(X).
6512                reachable(Y) :- reachable(X), edge(X, Y).
6513                audited(X) :- base(X).
6514                disconnected(X) :- disconnected_seed(X).
6515
6516                :- audited(99).
6517                ?- reachable(X).
6518            "#,
6519        )?;
6520
6521        let full = program.ordinary_plan("resident reachability test")?;
6522        let full_heads = full
6523            .rules_by_scc
6524            .iter()
6525            .flatten()
6526            .map(|rule| rule.head.as_str())
6527            .collect::<std::collections::BTreeSet<_>>();
6528        assert!(full_heads.contains("disconnected"));
6529
6530        let certified = program.resident_certified_plan()?;
6531        let resident = certified.plan();
6532        let resident_heads = resident
6533            .rules_by_scc
6534            .iter()
6535            .flatten()
6536            .map(|rule| rule.head.as_str())
6537            .collect::<std::collections::BTreeSet<_>>();
6538
6539        assert!(resident_heads.contains("reachable"));
6540        assert!(resident_heads.contains("audited"));
6541        assert!(resident_heads.contains("__xlog_constraint_0"));
6542        assert!(resident_heads.contains("__xlog_query_0"));
6543        assert!(!resident_heads.contains("disconnected"));
6544        assert!(resident.rules_by_scc.len() < full.rules_by_scc.len());
6545        assert!(resident
6546            .sccs
6547            .iter()
6548            .enumerate()
6549            .all(|(index, scc)| scc.id == index as u32));
6550        assert!(resident
6551            .strata
6552            .iter()
6553            .flat_map(|stratum| &stratum.sccs)
6554            .all(|scc| (*scc as usize) < resident.sccs.len()));
6555        assert_eq!(resident.generated_query_rules.len(), 1);
6556        let query = &resident.generated_query_rules[0];
6557        assert_eq!(query.query_index, 0);
6558        assert_eq!(
6559            resident.rules_by_scc[query.scc_index][query.rule_index].head,
6560            "__xlog_query_0"
6561        );
6562
6563        let full_heads_after_certification = full
6564            .rules_by_scc
6565            .iter()
6566            .flatten()
6567            .map(|rule| rule.head.as_str())
6568            .collect::<std::collections::BTreeSet<_>>();
6569        assert!(full_heads_after_certification.contains("disconnected"));
6570        Ok(())
6571    }
6572
6573    #[test]
6574    fn resident_dependency_closure_fails_closed_on_missing_duplicate_or_ambiguous_proof(
6575    ) -> Result<()> {
6576        fn plan_structure(
6577            plan: &ExecutionPlan,
6578        ) -> (
6579            Vec<(u32, Vec<String>)>,
6580            Vec<(u32, Vec<u32>)>,
6581            Vec<Vec<String>>,
6582            Vec<(usize, usize, usize)>,
6583        ) {
6584            (
6585                plan.sccs
6586                    .iter()
6587                    .map(|scc| (scc.id, scc.predicates.clone()))
6588                    .collect(),
6589                plan.strata
6590                    .iter()
6591                    .map(|stratum| (stratum.id, stratum.sccs.clone()))
6592                    .collect(),
6593                plan.rules_by_scc
6594                    .iter()
6595                    .map(|rules| rules.iter().map(|rule| rule.head.clone()).collect())
6596                    .collect(),
6597                plan.generated_query_rules
6598                    .iter()
6599                    .map(|query| (query.query_index, query.scc_index, query.rule_index))
6600                    .collect(),
6601            )
6602        }
6603
6604        let program = LogicProgram::compile(
6605            r#"
6606                pred input(u32).
6607                pred output(u32).
6608                input(1).
6609                output(X) :- input(X).
6610                ?- output(X).
6611            "#,
6612        )?;
6613        let full = program.ordinary_plan("resident fail-closed test")?;
6614
6615        let mut missing = full.clone();
6616        missing.generated_query_rules.clear();
6617        assert_eq!(
6618            plan_structure(&program.resident_dependency_closed_plan(&missing)),
6619            plan_structure(&missing)
6620        );
6621
6622        let mut duplicate = full.clone();
6623        duplicate
6624            .generated_query_rules
6625            .push(duplicate.generated_query_rules[0].clone());
6626        assert_eq!(
6627            plan_structure(&program.resident_dependency_closed_plan(&duplicate)),
6628            plan_structure(&duplicate)
6629        );
6630
6631        let mut ambiguous = full.clone();
6632        let duplicated_rule = ambiguous
6633            .rules_by_scc
6634            .iter()
6635            .flatten()
6636            .find(|rule| rule.head == "output")
6637            .expect("output rule")
6638            .clone();
6639        let duplicate_scc = ambiguous.sccs.len() as u32;
6640        ambiguous.sccs.push(xlog_ir::Scc {
6641            id: duplicate_scc,
6642            predicates: vec!["output".into()],
6643            is_recursive: false,
6644        });
6645        ambiguous.rules_by_scc.push(vec![duplicated_rule]);
6646        ambiguous.strata.push(xlog_ir::Stratum {
6647            id: ambiguous.strata.len() as u32,
6648            sccs: vec![duplicate_scc],
6649        });
6650        assert_eq!(
6651            plan_structure(&program.resident_dependency_closed_plan(&ambiguous)),
6652            plan_structure(&ambiguous)
6653        );
6654
6655        let mut missing_nonroot_rel_id = full.clone();
6656        missing_nonroot_rel_id
6657            .rules_by_scc
6658            .iter_mut()
6659            .flatten()
6660            .find(|rule| rule.head == "output")
6661            .expect("output rule")
6662            .head = "missing_nonroot_rel_id".into();
6663        assert!(program
6664            .try_resident_dependency_closed_plan(&missing_nonroot_rel_id)
6665            .is_none());
6666        assert_eq!(
6667            plan_structure(&program.resident_dependency_closed_plan(&missing_nonroot_rel_id)),
6668            plan_structure(&missing_nonroot_rel_id)
6669        );
6670
6671        let no_query = LogicProgram::compile(
6672            r#"
6673                pred input(u32).
6674                pred output(u32).
6675                input(1).
6676                output(X) :- input(X).
6677            "#,
6678        )?;
6679        let no_query_full = no_query.ordinary_plan("resident no-query test")?;
6680        assert_eq!(
6681            plan_structure(&no_query.resident_dependency_closed_plan(no_query_full)),
6682            plan_structure(no_query_full)
6683        );
6684        Ok(())
6685    }
6686
6687    #[test]
6688    fn compile_finalizer_preserves_and_replays_deterministic_certification_errors() -> Result<()> {
6689        let mut program = LogicProgram::compile(
6690            r#"
6691                pred output(u32).
6692                output(7).
6693                ?- output(X).
6694            "#,
6695        )?;
6696        program.reusable_state_identity = Arc::new(LogicProgramIdentity::new());
6697        let first = program
6698            .reusable_state_identity
6699            .get_or_init_resident_certification(|| -> Result<ResidentGraphCertifiedPlan> {
6700                Err(XlogError::Execution(
6701                    "deterministic certification failure".into(),
6702                ))
6703            })
6704            .expect_err("injected certification must fail");
6705        let program = program.finalize_compilation();
6706        let second = program
6707            .resident_certified_plan()
6708            .expect_err("cached certification must fail identically");
6709
6710        assert_eq!(first.to_string(), second.to_string());
6711        assert_eq!(program.resident_certification_initializations(), 1);
6712        Ok(())
6713    }
6714
6715    #[test]
6716    fn resident_certification_cache_is_ordinary_only_and_caches_declines_without_policy(
6717    ) -> Result<()> {
6718        let _env_lock = resident_env_lock().lock().expect("resident env lock");
6719        for (name, value) in [
6720            ("XLOG_DISABLE_RESIDENT_RECURSION", "1"),
6721            ("XLOG_USE_RESIDENT_RECURSION", "1"),
6722            ("XLOG_REQUIRE_RESIDENT_RECURSION", "1"),
6723        ] {
6724            let program = {
6725                let _env = ResidentEnvGuard::set(&[(name, value)]);
6726                LogicProgram::compile(
6727                    r#"
6728                        pred input(u32).
6729                        pred output(u32).
6730                        output(X) :- input(X).
6731                        ?- output(X).
6732                    "#,
6733                )?
6734            };
6735            assert_eq!(program.resident_certification_initializations(), 1);
6736        }
6737
6738        let epistemic = LogicProgram::compile(
6739            r#"
6740                pred p(u32). pred q(u32).
6741                p(1). q(X) :- p(X), know p(X). ?- q(X).
6742            "#,
6743        )?;
6744        assert!(!matches!(epistemic.plan, LogicExecutionPlan::Ordinary(_)));
6745        assert!(epistemic.resident_certified_plan().is_err());
6746        assert_eq!(epistemic.resident_certification_initializations(), 0);
6747
6748        let reduced_ordinary = LogicProgram::compile(
6749            r#"
6750                #pragma epistemic_mode = faeel
6751                pred node(u32).
6752                pred seed(u32, u32).
6753                pred trust(u32, u32).
6754                pred reach(u32, u32).
6755                node(1). node(2). node(3).
6756                seed(1, 2).
6757                reach(X, Y) :- seed(X, Y).
6758                reach(X, Z) :- reach(X, Y), trust(Y, Z).
6759                trust(2, 3) :- know reach(1, 2).
6760                trust(3, 1) :- know reach(3, 3).
6761                ?- reach(X, Y).
6762            "#,
6763        )?;
6764        assert!(matches!(
6765            reduced_ordinary.plan,
6766            LogicExecutionPlan::Ordinary(_)
6767        ));
6768        assert_eq!(reduced_ordinary.resident_certification_initializations(), 1);
6769
6770        let unsupported = LogicProgram::compile(
6771            r#"
6772                pred unsupported(f64).
6773                unsupported(7.5).
6774                ?- unsupported(X).
6775            "#,
6776        )?;
6777        let first = unsupported.resident_certified_plan()?;
6778        let second = unsupported.resident_certified_plan()?;
6779        assert!(Arc::ptr_eq(&first, &second));
6780        assert!(!first.certificate().is_supported());
6781        assert_eq!(unsupported.resident_certification_initializations(), 1);
6782        Ok(())
6783    }
6784
6785    fn median_seconds(samples: &mut [f64]) -> f64 {
6786        assert!(!samples.is_empty());
6787        samples.sort_by(f64::total_cmp);
6788        samples[samples.len() / 2]
6789    }
6790
6791    #[test]
6792    #[ignore = "requires the exact external issue corpus checkout and CUDA"]
6793    fn pinned_corpus_prepares_resident_graph_without_launching_it() -> Result<()> {
6794        let _env_lock = resident_env_lock().lock().expect("resident env lock");
6795        let corpus = std::path::PathBuf::from(
6796            std::env::var("XLOG_PINNED_CORPUS_ROOT")
6797                .expect("XLOG_PINNED_CORPUS_ROOT must name the pinned corpus checkout"),
6798        );
6799        assert_exact_clean_corpus(&corpus);
6800        let program = corpus_program(&corpus)?;
6801        let plan = program.ordinary_plan("resident graph preflight")?;
6802        let certificate = ResidentGraphRouteCertificate::inspect(plan, &schema_catalog(&program))?;
6803        assert!(certificate.is_supported(), "{:#?}", certificate.declines());
6804
6805        let Some(provider) = pinned_corpus_test_provider() else {
6806            return Ok(());
6807        };
6808        let resident_provider =
6809            LogicProgram::resident_provider_view(&provider).map_err(|reason| {
6810                XlogError::Execution(format!("resident provider preflight declined: {reason:?}"))
6811            })?;
6812        let executor =
6813            program.prepare_resident_executor(&resident_provider, HashMap::new(), false, plan)?;
6814        let runtime = resident_provider
6815            .memory()
6816            .runtime()
6817            .expect("resident provider view must own an async runtime");
6818        let graph_before = runtime.conditional_graph_stats();
6819        let allocated_before = resident_provider.memory().allocated_bytes();
6820
6821        let prepared = executor
6822            .prepare_resident_graph(plan, &certificate, ResidentGraphPrepareOptions::default())
6823            .map_err(LogicProgram::resident_execution_error)?;
6824        let report = prepared.preflight_report();
6825        let graph_after = runtime.conditional_graph_stats();
6826        assert_eq!(graph_after.launches, graph_before.launches);
6827        assert_eq!(
6828            graph_after.terminal_synchronizations,
6829            graph_before.terminal_synchronizations
6830        );
6831        assert_eq!(
6832            resident_provider
6833                .memory()
6834                .allocated_bytes()
6835                .saturating_sub(allocated_before),
6836            report.tracked_device_allocation_bytes
6837        );
6838        assert!(report.relation_capacity > 0);
6839        assert_eq!(report.parent_graph_nodes, 5);
6840        assert_eq!(report.conditional_while_nodes, 2);
6841        assert_eq!(
6842            report.parent_graph_node_kinds,
6843            vec![
6844                CudaGraphNodeKind::Kernel,
6845                CudaGraphNodeKind::Conditional,
6846                CudaGraphNodeKind::Kernel,
6847                CudaGraphNodeKind::Conditional,
6848                CudaGraphNodeKind::Kernel,
6849            ]
6850        );
6851        assert_eq!(
6852            report.conditional_body_node_kinds,
6853            vec![
6854                vec![CudaGraphNodeKind::Kernel],
6855                vec![CudaGraphNodeKind::Kernel],
6856            ]
6857        );
6858        assert_eq!(report.conditional_body_kernel_counts, vec![1, 1]);
6859        assert_eq!(report.hierarchical_graph_nodes, 7);
6860        eprintln!(
6861            "resident corpus preflight: capacity={} estimated_bytes={} available_bytes={} tracked_allocated_bytes={} parent_nodes={} conditional_while_nodes={}",
6862            report.relation_capacity,
6863            report.estimated_required_bytes,
6864            report.available_bytes_at_admission,
6865            report.tracked_device_allocation_bytes,
6866            report.parent_graph_nodes,
6867            report.conditional_while_nodes,
6868        );
6869        drop(prepared);
6870        assert_eq!(
6871            runtime.conditional_graph_stats().launches,
6872            graph_before.launches
6873        );
6874        Ok(())
6875    }
6876
6877    #[test]
6878    #[ignore = "requires the exact external issue corpus checkout and CUDA"]
6879    fn pinned_corpus_certifies_and_runs_through_the_resident_production_path() -> Result<()> {
6880        fn fallback_scan_filter_counts(node: &RirNode) -> (usize, usize) {
6881            match node {
6882                RirNode::Unit | RirNode::TensorMaskedJoin { .. } => (0, 0),
6883                RirNode::Scan { .. } => (1, 0),
6884                RirNode::Filter { input, .. } => {
6885                    let (scans, filters) = fallback_scan_filter_counts(input);
6886                    (scans, filters + 1)
6887                }
6888                RirNode::Project { input, .. }
6889                | RirNode::GroupBy { input, .. }
6890                | RirNode::Distinct { input, .. } => fallback_scan_filter_counts(input),
6891                RirNode::Join { left, right, .. } | RirNode::Diff { left, right } => {
6892                    let (left_scans, left_filters) = fallback_scan_filter_counts(left);
6893                    let (right_scans, right_filters) = fallback_scan_filter_counts(right);
6894                    (left_scans + right_scans, left_filters + right_filters)
6895                }
6896                RirNode::ChainJoin { fallback, .. } | RirNode::MultiWayJoin { fallback, .. } => {
6897                    fallback_scan_filter_counts(fallback)
6898                }
6899                RirNode::Union { inputs } => inputs.iter().fold((0, 0), |total, input| {
6900                    let current = fallback_scan_filter_counts(input);
6901                    (total.0 + current.0, total.1 + current.1)
6902                }),
6903                RirNode::Fixpoint {
6904                    base, recursive, ..
6905                } => {
6906                    let (base_scans, base_filters) = fallback_scan_filter_counts(base);
6907                    let (recursive_scans, recursive_filters) =
6908                        fallback_scan_filter_counts(recursive);
6909                    (
6910                        base_scans + recursive_scans,
6911                        base_filters + recursive_filters,
6912                    )
6913                }
6914            }
6915        }
6916
6917        let _env_lock = resident_env_lock().lock().expect("resident env lock");
6918        let corpus = std::path::PathBuf::from(
6919            std::env::var("XLOG_PINNED_CORPUS_ROOT")
6920                .expect("XLOG_PINNED_CORPUS_ROOT must name the pinned corpus checkout"),
6921        );
6922        assert_exact_clean_corpus(&corpus);
6923        let compile_and_certification_started = std::time::Instant::now();
6924        let program = corpus_program(&corpus)?;
6925        let compile_and_certification_seconds =
6926            compile_and_certification_started.elapsed().as_secs_f64();
6927        assert_eq!(
6928            program.resident_certification_initializations(),
6929            1,
6930            "ordinary compilation must eagerly seed one resident certification"
6931        );
6932        let plan = program.ordinary_plan("resident graph capability certificate")?;
6933        assert_eq!(plan.sccs.iter().filter(|scc| scc.is_recursive).count(), 2);
6934        assert_eq!(
6935            plan.sccs.iter().filter(|scc| !scc.is_recursive).count(),
6936            1_751
6937        );
6938        assert_eq!(plan.rules_by_scc.iter().map(Vec::len).sum::<usize>(), 4_559);
6939        let projected_reference_plan = program.resident_certified_plan()?.plan().clone();
6940        assert!(projected_reference_plan.sccs.len() < plan.sccs.len());
6941        assert!(
6942            projected_reference_plan
6943                .rules_by_scc
6944                .iter()
6945                .map(Vec::len)
6946                .sum::<usize>()
6947                < plan.rules_by_scc.iter().map(Vec::len).sum::<usize>()
6948        );
6949        let chain_fallbacks = plan
6950            .rules_by_scc
6951            .iter()
6952            .enumerate()
6953            .flat_map(|(scc_index, rules)| {
6954                rules.iter().filter_map(move |rule| {
6955                    let RirNode::ChainJoin { fallback, .. } = &rule.body else {
6956                        return None;
6957                    };
6958                    let (scans, filters) = fallback_scan_filter_counts(fallback);
6959                    Some((
6960                        scc_index,
6961                        plan.sccs[scc_index].is_recursive,
6962                        rule.head.clone(),
6963                        scans,
6964                        filters,
6965                    ))
6966                })
6967            })
6968            .collect::<Vec<_>>();
6969        let projected_chain_fallbacks = projected_reference_plan
6970            .rules_by_scc
6971            .iter()
6972            .enumerate()
6973            .flat_map(|(scc_index, rules)| {
6974                let is_recursive = projected_reference_plan.sccs[scc_index].is_recursive;
6975                rules.iter().filter_map(move |rule| {
6976                    let RirNode::ChainJoin { fallback, .. } = &rule.body else {
6977                        return None;
6978                    };
6979                    let (scans, filters) = fallback_scan_filter_counts(fallback);
6980                    Some((scc_index, is_recursive, rule.head.clone(), scans, filters))
6981                })
6982            })
6983            .collect::<Vec<_>>();
6984        eprintln!(
6985            "chain fallback inventory: routes={} scans={} filters={} details={chain_fallbacks:?}",
6986            chain_fallbacks.len(),
6987            chain_fallbacks.iter().map(|route| route.3).sum::<usize>(),
6988            chain_fallbacks.iter().map(|route| route.4).sum::<usize>()
6989        );
6990
6991        let expected_routes = independent_route_descriptors(&program, plan);
6992        let certificate = ResidentGraphRouteCertificate::inspect(plan, &schema_catalog(&program))?;
6993        assert!(certificate.is_supported(), "{:#?}", certificate.declines());
6994        assert!(certificate.matches_plan(plan)?);
6995        let mut covered_structural_bindings = BTreeSet::new();
6996        let mut covered_physical_routes = BTreeSet::new();
6997        for descriptor in certificate.covered_route_descriptors() {
6998            if descriptor.starts_with("plan;") {
6999                covered_structural_bindings.insert(descriptor.clone());
7000            } else if descriptor.starts_with("scc=") {
7001                covered_physical_routes.insert(descriptor.clone());
7002            } else {
7003                panic!("unknown resident certificate descriptor class: {descriptor}");
7004            }
7005        }
7006        assert!(!covered_structural_bindings.is_empty());
7007        assert!(!covered_physical_routes.is_empty());
7008        assert_eq!(covered_physical_routes, expected_routes);
7009
7010        let Some(provider) = pinned_corpus_test_provider() else {
7011            return Ok(());
7012        };
7013        let baseline = {
7014            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
7015            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
7016        };
7017        let baseline_snapshot = snapshot_query_results(provider.as_ref(), &baseline)?;
7018        let baseline_stats = baseline.stats.as_ref().expect("baseline profile");
7019        let baseline_scans = op_count(baseline_stats, "scan");
7020        let baseline_filters = op_count(baseline_stats, "filter");
7021        let chain_fallback_scan_equivalents = baseline_stats.chain_fallback_scan_equivalents;
7022        let chain_fallback_filter_equivalents = baseline_stats.chain_fallback_filter_equivalents;
7023        let full_semantic_scans = baseline_scans as u64 + chain_fallback_scan_equivalents;
7024        let full_semantic_filters = baseline_filters as u64 + chain_fallback_filter_equivalents;
7025        assert_eq!(
7026            chain_fallback_scan_equivalents,
7027            chain_fallbacks
7028                .iter()
7029                .map(|route| route.3 as u64)
7030                .sum::<u64>()
7031        );
7032        assert_eq!(
7033            chain_fallback_filter_equivalents,
7034            chain_fallbacks
7035                .iter()
7036                .map(|route| route.4 as u64)
7037                .sum::<u64>()
7038        );
7039        eprintln!(
7040            "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={}",
7041            baseline_stats.wcoj_triangle_dispatch_count,
7042            baseline_stats.wcoj_4cycle_dispatch_count,
7043            baseline_stats.free_join_dispatch_count,
7044            baseline_stats.factorized_delta_dispatch_count
7045        );
7046        let mut projected_program = program.clone();
7047        projected_program.plan =
7048            LogicExecutionPlan::Ordinary(Box::new(projected_reference_plan.clone()));
7049        let projected_baseline = {
7050            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
7051            projected_program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
7052        };
7053        assert_eq!(
7054            snapshot_query_results(provider.as_ref(), &projected_baseline)?,
7055            baseline_snapshot,
7056            "dependency-closed ordinary reference changed full-plan query semantics"
7057        );
7058        let projected_stats = projected_baseline
7059            .stats
7060            .as_ref()
7061            .expect("dependency-closed ordinary reference profile");
7062        let projected_scans = op_count(projected_stats, "scan");
7063        let projected_filters = op_count(projected_stats, "filter");
7064        assert_eq!(
7065            projected_stats.chain_fallback_scan_equivalents,
7066            projected_chain_fallbacks
7067                .iter()
7068                .map(|route| route.3 as u64)
7069                .sum::<u64>()
7070        );
7071        assert_eq!(
7072            projected_stats.chain_fallback_filter_equivalents,
7073            projected_chain_fallbacks
7074                .iter()
7075                .map(|route| route.4 as u64)
7076                .sum::<u64>()
7077        );
7078        let expected_semantic_scans =
7079            projected_scans as u64 + projected_stats.chain_fallback_scan_equivalents;
7080        let expected_semantic_filters =
7081            projected_filters as u64 + projected_stats.chain_fallback_filter_equivalents;
7082        eprintln!(
7083            "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}",
7084            projected_stats.chain_fallback_scan_equivalents,
7085            projected_stats.chain_fallback_filter_equivalents,
7086        );
7087        drop(projected_baseline);
7088        assert!(
7089            baseline_scans >= 9_000,
7090            "unexpected baseline scan count: {baseline_scans}"
7091        );
7092        assert!(
7093            baseline_filters >= 7_000,
7094            "unexpected baseline filter count: {baseline_filters}"
7095        );
7096        drop(baseline);
7097        assert_eq!(
7098            program.resident_certification_initializations(),
7099            1,
7100            "external certificate audits and the disabled-resident baseline must reuse the compile-time certification"
7101        );
7102
7103        let mut resident_seconds = Vec::with_capacity(5);
7104        let mut device_seconds = Vec::with_capacity(5);
7105        for run in 0..5 {
7106            let started = std::time::Instant::now();
7107            let resident = {
7108                let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
7109                program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
7110            };
7111            assert_eq!(
7112                program.resident_certification_initializations(),
7113                1,
7114                "resident corpus run {run} must reuse the single cached certification"
7115            );
7116            resident_seconds.push(started.elapsed().as_secs_f64());
7117            assert_eq!(
7118                snapshot_query_results(provider.as_ref(), &resident)?,
7119                baseline_snapshot,
7120                "resident corpus run {run} changed query semantics"
7121            );
7122
7123            let resident_stats = resident.stats.as_ref().expect("resident profile");
7124            let resident_physical_scans = op_count(resident_stats, "scan");
7125            let resident_physical_filters = op_count(resident_stats, "filter");
7126            let graph = resident_stats
7127                .resident_graph
7128                .as_ref()
7129                .expect("resident selection telemetry");
7130            eprintln!(
7131                "resident operation profile run {run}: semantic_scans={} semantic_filters={} physical_scans={resident_physical_scans} physical_filters={resident_physical_filters}",
7132                graph.semantic_scan_invocations, graph.semantic_filter_invocations
7133            );
7134            assert_eq!(graph.semantic_scan_invocations, expected_semantic_scans);
7135            assert_eq!(graph.semantic_filter_invocations, expected_semantic_filters);
7136            assert_eq!(
7137                resident_physical_scans as u64,
7138                graph.device_scan_invocations
7139            );
7140            assert_eq!(
7141                resident_physical_filters as u64,
7142                graph.device_filter_invocations
7143            );
7144            assert_eq!(
7145                graph.selection,
7146                ResidentGraphSelectionKind::ResidentConditionalGraph
7147            );
7148            assert_eq!(graph.conditional_graph_launches, 1);
7149            assert_eq!(graph.terminal_synchronizations, 1);
7150            assert_eq!(graph.host_iterations, 0);
7151            assert_eq!(graph.host_allocations, 0);
7152            assert_eq!(graph.host_status_injections, 0);
7153            assert_eq!(graph.deterministic_d2h_violations, 0);
7154            assert_eq!(graph.host_dispatched_scan_ops, 0);
7155            assert_eq!(graph.host_dispatched_filter_ops, 0);
7156            assert!(graph.device_scan_invocations >= graph.semantic_scan_invocations);
7157            assert!(graph.device_filter_invocations >= graph.semantic_filter_invocations);
7158            assert_eq!(
7159                graph.deferred_profile.timed_scan_filter_invocations,
7160                graph.device_scan_invocations + graph.device_filter_invocations
7161            );
7162            assert!(graph.deferred_profile.device_elapsed_ns > 0);
7163            assert_eq!(graph.deferred_profile.final_sync_misattributed_ns, 0);
7164            device_seconds.push(graph.deferred_profile.device_elapsed_ns as f64 / 1_000_000_000.0);
7165            assert_eq!(graph.core_transfers.tracked_htod_calls, 0);
7166            assert_eq!(graph.core_transfers.tracked_htod_bytes, 0);
7167            assert_eq!(graph.core_transfers.tracked_dtoh_calls, 0);
7168            assert_eq!(graph.core_transfers.tracked_dtoh_bytes, 0);
7169            assert_eq!(graph.core_transfers.provider_dtoh_calls, 0);
7170            assert_eq!(graph.core_transfers.untracked_metadata_dtoh_calls, 0);
7171            assert_eq!(graph.final_observation.dtoh_calls, 1);
7172            assert_eq!(
7173                graph.final_observation.dtoh_bytes,
7174                60 + 8 * graph.staged_store_mutations
7175            );
7176            assert_eq!(graph.final_observation.pinned_receipts, 1);
7177            let json = resident_stats.format_json();
7178            assert!(json.contains("\"resident_graph\""), "{json}");
7179            assert!(
7180                json.contains("\"selection\":\"resident_conditional_graph\""),
7181                "{json}"
7182            );
7183            assert!(
7184                json.contains(&format!(
7185                    "\"semantic_scan_invocations\":{expected_semantic_scans}"
7186                )),
7187                "{json}"
7188            );
7189            assert!(
7190                json.contains(&format!(
7191                    "\"semantic_filter_invocations\":{expected_semantic_filters}"
7192                )),
7193                "{json}"
7194            );
7195            drop(resident);
7196        }
7197        assert_eq!(program.resident_certification_initializations(), 1);
7198        let compile_plus_first_resident_seconds =
7199            compile_and_certification_seconds + resident_seconds[0];
7200        let max_seconds = resident_seconds.iter().copied().fold(0.0_f64, f64::max);
7201        let mut sorted_resident_seconds = resident_seconds.clone();
7202        let median_seconds = median_seconds(&mut sorted_resident_seconds);
7203        eprintln!(
7204            "resident corpus latency: compile_and_certification_seconds={compile_and_certification_seconds:.6} compile_plus_first_resident_seconds={compile_plus_first_resident_seconds:.6} end_to_end_seconds={resident_seconds:?} device_event_seconds={device_seconds:?} median_end_to_end_seconds={median_seconds:.6} max_end_to_end_seconds={max_seconds:.6}"
7205        );
7206        assert!(
7207            median_seconds <= 1.25,
7208            "five-run resident corpus median {median_seconds:.6}s exceeds 1.25s: {resident_seconds:?}"
7209        );
7210        assert!(
7211            max_seconds <= 1.75,
7212            "five-run resident corpus max {max_seconds:.6}s exceeds 1.75s: {resident_seconds:?}"
7213        );
7214        Ok(())
7215    }
7216
7217    #[test]
7218    #[ignore = "requires the exact external issue corpus checkout and serialized CUDA"]
7219    fn pinned_corpus_resident_latency_phase_diagnostic() -> Result<()> {
7220        let _env_lock = resident_env_lock().lock().expect("resident env lock");
7221        let corpus = std::path::PathBuf::from(
7222            std::env::var("XLOG_PINNED_CORPUS_ROOT")
7223                .expect("XLOG_PINNED_CORPUS_ROOT must name the pinned corpus checkout"),
7224        );
7225        assert_exact_clean_corpus(&corpus);
7226        let compile_started = std::time::Instant::now();
7227        let program = corpus_program(&corpus)?;
7228        eprintln!(
7229            "resident latency setup: compile_ns={}",
7230            u64::try_from(compile_started.elapsed().as_nanos()).unwrap_or(u64::MAX)
7231        );
7232        let Some(provider) = pinned_corpus_test_provider() else {
7233            return Ok(());
7234        };
7235        let baseline = {
7236            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
7237            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
7238        };
7239        let expected = snapshot_query_results(provider.as_ref(), &baseline)?;
7240        drop(baseline);
7241
7242        RESIDENT_LATENCY_SAMPLE.store(0, Ordering::Relaxed);
7243        for run in 0..5 {
7244            let evaluate_started = std::time::Instant::now();
7245            let resident = {
7246                let _env = ResidentEnvGuard::set(&[
7247                    ("XLOG_REQUIRE_RESIDENT_RECURSION", "1"),
7248                    (RESIDENT_LATENCY_DIAGNOSTICS_ENV, "1"),
7249                ]);
7250                program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
7251            };
7252            let evaluate_return_ns =
7253                u64::try_from(evaluate_started.elapsed().as_nanos()).unwrap_or(u64::MAX);
7254            assert_eq!(
7255                snapshot_query_results(provider.as_ref(), &resident)?,
7256                expected,
7257                "resident latency diagnostic run {run} changed query semantics"
7258            );
7259            let graph = resident
7260                .stats
7261                .as_ref()
7262                .and_then(|stats| stats.resident_graph.as_ref())
7263                .expect("resident latency diagnostic telemetry");
7264            assert_eq!(
7265                graph.selection,
7266                ResidentGraphSelectionKind::ResidentConditionalGraph
7267            );
7268            let query_buffers = resident.queries.len();
7269            let manager_bytes_before_result_drop = provider.memory().allocated_bytes();
7270            let result_drop_started = std::time::Instant::now();
7271            drop(resident);
7272            let result_drop_ns =
7273                u64::try_from(result_drop_started.elapsed().as_nanos()).unwrap_or(u64::MAX);
7274            let manager_bytes_after_result_drop = provider.memory().allocated_bytes();
7275            let result_manager_bytes_released =
7276                manager_bytes_before_result_drop.saturating_sub(manager_bytes_after_result_drop);
7277            eprintln!(
7278                "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"
7279            );
7280        }
7281        Ok(())
7282    }
7283
7284    #[test]
7285    #[ignore = "requires a serialized release-mode CUDA acceptance run"]
7286    fn resident_disconnected_four_thousand_rule_scaling_acceptance() -> Result<()> {
7287        let _env_lock = resident_env_lock().lock().expect("resident env lock");
7288        let corpus = std::path::PathBuf::from(
7289            std::env::var("XLOG_PINNED_CORPUS_ROOT")
7290                .expect("XLOG_PINNED_CORPUS_ROOT must name the pinned corpus checkout"),
7291        );
7292        assert_exact_clean_corpus(&corpus);
7293        let base_program = corpus_program(&corpus)?;
7294        let entry = corpus.join("scenarios/acceptance/issue1/q01_blind.xlog");
7295        let mut augmented_source = std::fs::read_to_string(&entry).map_err(|error| {
7296            XlogError::Execution(format!("failed to read {}: {error}", entry.display()))
7297        })?;
7298        augmented_source.push_str("\npred disconnected_seed(u32).\n");
7299        for family in 0..4_000 {
7300            augmented_source.push_str(&format!("pred disconnected_family_{family}(u32).\n"));
7301            augmented_source.push_str(&format!(
7302                "disconnected_family_{family}(X) :- disconnected_seed(X).\n"
7303            ));
7304        }
7305        let resolver = xlog_logic::compile::load_modules(&entry, vec![corpus.join("programs")])
7306            .map_err(|error| XlogError::Compilation(error.to_string()))?;
7307        let augmented_program = LogicProgram::compile_with_resolver(&augmented_source, &resolver)?;
7308        let Some(provider) = pinned_corpus_test_provider() else {
7309            return Ok(());
7310        };
7311
7312        let mut base_seconds = Vec::with_capacity(5);
7313        let mut expected_snapshot = None;
7314        let mut expected_profile = None;
7315        for run in 0..5 {
7316            let started = std::time::Instant::now();
7317            let result = {
7318                let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
7319                base_program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
7320            };
7321            base_seconds.push(started.elapsed().as_secs_f64());
7322            let snapshot = snapshot_query_results(provider.as_ref(), &result)?;
7323            if let Some(expected) = &expected_snapshot {
7324                assert_eq!(&snapshot, expected, "base resident run {run} drifted");
7325            } else {
7326                expected_snapshot = Some(snapshot);
7327            }
7328            let stats = result.stats.as_ref().expect("base resident profile");
7329            let graph = stats
7330                .resident_graph
7331                .as_ref()
7332                .expect("base resident graph telemetry");
7333            assert_eq!(
7334                graph.selection,
7335                ResidentGraphSelectionKind::ResidentConditionalGraph
7336            );
7337            assert_eq!(graph.conditional_graph_launches, 1);
7338            assert_eq!(
7339                op_count(stats, "scan") as u64,
7340                graph.device_scan_invocations
7341            );
7342            assert_eq!(
7343                op_count(stats, "filter") as u64,
7344                graph.device_filter_invocations
7345            );
7346            let profile = (
7347                strata_op_profile(stats),
7348                graph.device_scan_invocations,
7349                graph.device_filter_invocations,
7350                graph.semantic_scan_invocations,
7351                graph.semantic_filter_invocations,
7352                graph.deferred_profile.timed_scan_filter_invocations,
7353            );
7354            if let Some(expected) = &expected_profile {
7355                assert_eq!(&profile, expected, "base resident run {run} op drift");
7356            } else {
7357                expected_profile = Some(profile);
7358            }
7359            drop(result);
7360        }
7361
7362        let expected_snapshot = expected_snapshot.expect("base resident snapshot");
7363        let expected_profile = expected_profile.expect("base resident operation profile");
7364        let mut augmented_seconds = Vec::with_capacity(5);
7365        for run in 0..5 {
7366            let started = std::time::Instant::now();
7367            let result = {
7368                let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
7369                augmented_program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
7370            };
7371            augmented_seconds.push(started.elapsed().as_secs_f64());
7372            assert_eq!(
7373                snapshot_query_results(provider.as_ref(), &result)?,
7374                expected_snapshot,
7375                "disconnected family changed query output on run {run}"
7376            );
7377            let stats = result.stats.as_ref().expect("augmented resident profile");
7378            let graph = stats
7379                .resident_graph
7380                .as_ref()
7381                .expect("augmented resident graph telemetry");
7382            assert_eq!(
7383                graph.selection,
7384                ResidentGraphSelectionKind::ResidentConditionalGraph
7385            );
7386            assert_eq!(graph.conditional_graph_launches, 1);
7387            assert_eq!(
7388                op_count(stats, "scan") as u64,
7389                graph.device_scan_invocations
7390            );
7391            assert_eq!(
7392                op_count(stats, "filter") as u64,
7393                graph.device_filter_invocations
7394            );
7395            assert_eq!(
7396                (
7397                    strata_op_profile(stats),
7398                    graph.device_scan_invocations,
7399                    graph.device_filter_invocations,
7400                    graph.semantic_scan_invocations,
7401                    graph.semantic_filter_invocations,
7402                    graph.deferred_profile.timed_scan_filter_invocations,
7403                ),
7404                expected_profile,
7405                "disconnected family changed semantic or device op counts on run {run}"
7406            );
7407            drop(result);
7408        }
7409
7410        let base_median = median_seconds(&mut base_seconds);
7411        let augmented_median = median_seconds(&mut augmented_seconds);
7412        let allowed_delta = (base_median * 0.10).max(0.100);
7413        assert!(
7414            augmented_median - base_median <= allowed_delta,
7415            "disconnected 4,000-rule resident median delta {:.6}s exceeds {:.6}s: base={base_seconds:?} augmented={augmented_seconds:?}",
7416            augmented_median - base_median,
7417            allowed_delta,
7418        );
7419        Ok(())
7420    }
7421
7422    fn assert_required_resident_semantics(
7423        source: &str,
7424        case: &str,
7425        expected_query_schema: Option<&Schema>,
7426        expected_query_types: Option<&[ScalarType]>,
7427        expected_query_rows: Option<usize>,
7428    ) -> Result<()> {
7429        let _env_lock = resident_env_lock().lock().expect("resident env lock");
7430        let program = LogicProgram::compile(source)?;
7431        let Some(provider) = ground_term_encoding_test_provider() else {
7432            return Ok(());
7433        };
7434        let baseline = {
7435            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
7436            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
7437        };
7438        let expected = snapshot_query_results(provider.as_ref(), &baseline)?;
7439        if let Some(expected_query_schema) = expected_query_schema {
7440            assert_eq!(expected.len(), 1, "schema witness must have one query");
7441            assert_eq!(
7442                &expected[0].schema, expected_query_schema,
7443                "pre-change legacy query schema witness changed for {case}"
7444            );
7445        }
7446        if let Some(expected_query_types) = expected_query_types {
7447            assert_eq!(expected.len(), 1, "type witness must have one query");
7448            assert_eq!(
7449                expected[0]
7450                    .schema
7451                    .columns
7452                    .iter()
7453                    .map(|(_, scalar)| *scalar)
7454                    .collect::<Vec<_>>(),
7455                expected_query_types,
7456                "pre-change legacy query types changed for {case}"
7457            );
7458        }
7459        if let Some(expected_query_rows) = expected_query_rows {
7460            assert_eq!(expected.len(), 1, "row witness must have one query");
7461            assert_eq!(
7462                expected[0].rows.len(),
7463                expected_query_rows,
7464                "pre-change legacy query row count changed for {case}"
7465            );
7466        }
7467        drop(baseline);
7468        let resident = {
7469            let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
7470            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
7471        };
7472        assert_eq!(
7473            snapshot_query_results(provider.as_ref(), &resident)?,
7474            expected,
7475            "resident semantic case {case} diverged"
7476        );
7477        let graph = resident
7478            .stats
7479            .as_ref()
7480            .and_then(|stats| stats.resident_graph.as_ref())
7481            .expect("resident semantic telemetry");
7482        assert_eq!(
7483            graph.selection,
7484            ResidentGraphSelectionKind::ResidentConditionalGraph,
7485            "resident semantic case {case} did not use the production graph"
7486        );
7487        assert_eq!(graph.conditional_graph_launches, 1);
7488        assert_eq!(graph.core_transfers.tracked_htod_calls, 0);
7489        assert_eq!(graph.core_transfers.tracked_htod_bytes, 0);
7490        assert_eq!(graph.core_transfers.tracked_dtoh_calls, 0);
7491        assert_eq!(graph.core_transfers.tracked_dtoh_bytes, 0);
7492        assert_eq!(graph.core_transfers.provider_dtoh_calls, 0);
7493        assert_eq!(graph.core_transfers.untracked_metadata_dtoh_calls, 0);
7494        assert_eq!(graph.deterministic_d2h_violations, 0);
7495        assert_eq!(graph.final_observation.dtoh_calls, 1);
7496        assert_eq!(graph.final_observation.pinned_receipts, 1);
7497        Ok(())
7498    }
7499
7500    fn program_with_authored_query_prefix(
7501        source: &str,
7502        original_head: &str,
7503        authored_head: &str,
7504    ) -> Result<Program> {
7505        let mut program = xlog_logic::parse_program(source)?;
7506        let mut renamed_declarations = 0usize;
7507        for declaration in &mut program.predicates {
7508            if declaration.name == original_head {
7509                declaration.name = authored_head.to_string();
7510                renamed_declarations += 1;
7511            }
7512        }
7513        let mut renamed_rules = 0usize;
7514        for rule in &mut program.rules {
7515            if rule.head.predicate == original_head {
7516                rule.head.predicate = authored_head.to_string();
7517                renamed_rules += 1;
7518            }
7519        }
7520        let mut renamed_queries = 0usize;
7521        for query in &mut program.queries {
7522            if query.atom.predicate == original_head {
7523                query.atom.predicate = authored_head.to_string();
7524                renamed_queries += 1;
7525            }
7526        }
7527        assert_eq!(renamed_declarations, 1);
7528        assert!(renamed_rules >= 1);
7529        assert_eq!(renamed_queries, 1);
7530        Ok(program)
7531    }
7532
7533    fn compile_program_with_authored_query_prefix(
7534        source: &str,
7535        original_head: &str,
7536        authored_head: &str,
7537    ) -> Result<LogicProgram> {
7538        LogicProgram::compile_program(program_with_authored_query_prefix(
7539            source,
7540            original_head,
7541            authored_head,
7542        )?)
7543    }
7544
7545    fn assert_required_resident_authored_prefix_semantics(source: &str, case: &str) -> Result<()> {
7546        const AUTHORED_HEAD: &str = "__xlog_query_authored";
7547        let _env_lock = resident_env_lock().lock().expect("resident env lock");
7548        let program = compile_program_with_authored_query_prefix(source, "answer", AUTHORED_HEAD)?;
7549        let Some(provider) = ground_term_encoding_test_provider() else {
7550            return Ok(());
7551        };
7552        let explicit_schema = Schema::new(vec![("external_value".to_string(), ScalarType::Symbol)]);
7553        let baseline = {
7554            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
7555            program.evaluate_with_options(
7556                provider.clone(),
7557                HashMap::from([(
7558                    AUTHORED_HEAD.to_string(),
7559                    provider.create_empty_buffer(explicit_schema.clone())?,
7560                )]),
7561                true,
7562            )?
7563        };
7564        let expected = snapshot_query_results(provider.as_ref(), &baseline)?;
7565        assert_eq!(expected.len(), 1);
7566        drop(baseline);
7567        let resident = {
7568            let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
7569            program.evaluate_with_options(
7570                provider.clone(),
7571                HashMap::from([(
7572                    AUTHORED_HEAD.to_string(),
7573                    provider.create_empty_buffer(explicit_schema)?,
7574                )]),
7575                true,
7576            )?
7577        };
7578        assert_eq!(
7579            snapshot_query_results(provider.as_ref(), &resident)?,
7580            expected,
7581            "resident authored-prefix case {case} diverged"
7582        );
7583        let graph = resident
7584            .stats
7585            .as_ref()
7586            .and_then(|stats| stats.resident_graph.as_ref())
7587            .expect("resident authored-prefix telemetry");
7588        assert_eq!(
7589            graph.selection,
7590            ResidentGraphSelectionKind::ResidentConditionalGraph,
7591            "resident authored-prefix case {case} declined"
7592        );
7593        assert_eq!(graph.conditional_graph_launches, 1);
7594        Ok(())
7595    }
7596
7597    #[test]
7598    fn required_resident_preserves_programmatic_authored_query_prefix_empty_input() -> Result<()> {
7599        assert_required_resident_authored_prefix_semantics(
7600            r#"
7601                pred source(symbol).
7602                pred answer(symbol).
7603                answer(X) :- source(X).
7604                ?- answer(X).
7605            "#,
7606            "one authored rule",
7607        )
7608    }
7609
7610    #[test]
7611    fn required_resident_does_not_treat_programmatic_authored_prefix_rules_as_generated(
7612    ) -> Result<()> {
7613        assert_required_resident_authored_prefix_semantics(
7614            r#"
7615                pred left_source(symbol).
7616                pred right_source(symbol).
7617                pred answer(symbol).
7618                answer(X) :- left_source(X).
7619                answer(X) :- right_source(X).
7620                ?- answer(X).
7621            "#,
7622            "two authored rules",
7623        )
7624    }
7625
7626    #[test]
7627    fn compile_program_rejects_exact_generated_query_head_collision() -> Result<()> {
7628        let program = program_with_authored_query_prefix(
7629            r#"
7630                pred source(symbol).
7631                pred answer(symbol).
7632                answer(X) :- source(X).
7633                ?- answer(X).
7634            "#,
7635            "answer",
7636            "__xlog_query_0",
7637        )?;
7638        let error = match LogicProgram::compile_program(program) {
7639            Ok(_) => panic!("exact compiler-generated query head collision must be rejected"),
7640            Err(error) => error,
7641        };
7642        assert!(
7643            error
7644                .to_string()
7645                .contains("authored relation __xlog_query_0 collides with generated query head"),
7646            "unexpected collision error: {error}"
7647        );
7648        Ok(())
7649    }
7650
7651    #[test]
7652    fn compiler_generated_query_relation_validation_is_exact_and_host_only() -> Result<()> {
7653        let program = LogicProgram::compile(
7654            r#"
7655                pred source(symbol).
7656                ?- source(X).
7657            "#,
7658        )?;
7659
7660        let error = program
7661            .reject_compiler_generated_query_relation_names(["__xlog_query_0"], "persistent caller")
7662            .expect_err("an exact compiler-generated query head must be rejected");
7663        match error {
7664            XlogError::Execution(message) => assert_eq!(
7665                message,
7666                "persistent caller relation __xlog_query_0 collides with generated query head"
7667            ),
7668            other => panic!("expected typed execution rejection, got {other:?}"),
7669        }
7670
7671        program.reject_compiler_generated_query_relation_names(
7672            ["__xlog_query_authored"],
7673            "persistent caller",
7674        )?;
7675        Ok(())
7676    }
7677
7678    #[test]
7679    fn compiler_generated_query_relation_validation_rejects_provenance_mutation() -> Result<()> {
7680        let source = r#"
7681            pred source(symbol).
7682            ?- source(X).
7683        "#;
7684
7685        let mut omitted = LogicProgram::compile(source)?;
7686        let LogicExecutionPlan::Ordinary(plan) = &mut omitted.plan else {
7687            panic!("ordinary program must compile to an ordinary plan");
7688        };
7689        plan.generated_query_rules.clear();
7690        let error = omitted
7691            .reject_compiler_generated_query_relation_names(std::iter::empty(), "caller input")
7692            .expect_err("omitted compiler provenance must be rejected");
7693        assert!(error.to_string().contains(
7694            "compiler-generated query provenance count 0 does not match authored query count 1"
7695        ));
7696
7697        let mut repositioned = LogicProgram::compile(source)?;
7698        let LogicExecutionPlan::Ordinary(plan) = &mut repositioned.plan else {
7699            panic!("ordinary program must compile to an ordinary plan");
7700        };
7701        plan.generated_query_rules[0].query_index = 1;
7702        let error = repositioned
7703            .reject_compiler_generated_query_relation_names(std::iter::empty(), "caller input")
7704            .expect_err("repositioned compiler provenance must be rejected");
7705        assert!(error
7706            .to_string()
7707            .contains("compiler-generated query provenance position 0 carries query index 1"));
7708
7709        let mut renamed = LogicProgram::compile(source)?;
7710        let LogicExecutionPlan::Ordinary(plan) = &mut renamed.plan else {
7711            panic!("ordinary program must compile to an ordinary plan");
7712        };
7713        let provenance = &plan.generated_query_rules[0];
7714        plan.rules_by_scc[provenance.scc_index][provenance.rule_index].head =
7715            "__xlog_query_spoof".to_string();
7716        let error = renamed
7717            .reject_compiler_generated_query_relation_names(std::iter::empty(), "caller input")
7718            .expect_err("renamed compiler-generated query head must be rejected");
7719        assert!(error.to_string().contains(
7720            "compiler-generated query provenance 0 expects head __xlog_query_0 but references authored head __xlog_query_spoof"
7721        ));
7722        Ok(())
7723    }
7724
7725    #[test]
7726    fn cloned_program_shares_reusable_state_identity_but_recompile_does_not() -> Result<()> {
7727        let source = r#"
7728            pred source(symbol).
7729            ?- source(X).
7730        "#;
7731        let original = LogicProgram::compile(source)?;
7732        let cloned = original.clone();
7733        let recompiled = LogicProgram::compile(source)?;
7734
7735        cloned.validate_reusable_state_identity(
7736            &original.reusable_state_identity,
7737            "materialized cache",
7738        )?;
7739        let error = recompiled
7740            .validate_reusable_state_identity(
7741                &original.reusable_state_identity,
7742                "materialized cache",
7743            )
7744            .expect_err("independent compilation must have a distinct reusable-state identity");
7745        assert!(matches!(error, XlogError::Execution(_)));
7746        assert_eq!(
7747            error.to_string(),
7748            "Execution error: materialized cache belongs to a different compiled logic program"
7749        );
7750        Ok(())
7751    }
7752
7753    #[test]
7754    fn foreign_cache_and_runtime_are_rejected_before_evaluation_work() -> Result<()> {
7755        let source = r#"
7756            pred source(u32).
7757            pred out(u32).
7758            out(X) :- source(X).
7759            ?- out(X).
7760        "#;
7761        let program = LogicProgram::compile(source)?;
7762        let foreign_program = LogicProgram::compile(source)?;
7763        let Some(provider) = ground_term_encoding_test_provider() else {
7764            return Ok(());
7765        };
7766        let base_store = program.create_relation_store(provider.clone())?;
7767        let (_, cache) =
7768            program.evaluate_with_relation_store_and_cache(provider.clone(), &base_store, false)?;
7769        let allocations_before_cache = provider.memory().alloc_count();
7770
7771        let error = match foreign_program.evaluate_cached_relation_store(provider.clone(), &cache) {
7772            Ok(_) => panic!("independently compiled program must reject a foreign cache"),
7773            Err(error) => error,
7774        };
7775        assert_eq!(
7776            error.to_string(),
7777            "Execution error: materialized cache belongs to a different compiled logic program"
7778        );
7779        assert_eq!(provider.memory().alloc_count(), allocations_before_cache);
7780
7781        let mut runtime = program.create_session_runtime(provider.clone(), &base_store, false)?;
7782        let runtime_store_before = std::ptr::from_ref(runtime.executor.store());
7783        let source_version_before = runtime.executor.store().version("source");
7784        let allocations_before_runtime = provider.memory().alloc_count();
7785        let error =
7786            match foreign_program.evaluate_with_session_runtime(provider.clone(), &mut runtime) {
7787                Ok(_) => panic!("independently compiled program must reject a foreign runtime"),
7788                Err(error) => error,
7789            };
7790        assert_eq!(
7791            error.to_string(),
7792            "Execution error: session runtime belongs to a different compiled logic program"
7793        );
7794        assert_eq!(provider.memory().alloc_count(), allocations_before_runtime);
7795        assert_eq!(
7796            std::ptr::from_ref(runtime.executor.store()),
7797            runtime_store_before
7798        );
7799        assert_eq!(
7800            runtime.executor.store().version("source"),
7801            source_version_before
7802        );
7803        Ok(())
7804    }
7805
7806    #[test]
7807    fn foreign_reusable_state_is_rejected_before_delta_take_or_device_work() -> Result<()> {
7808        let source = r#"
7809            pred source(u32).
7810            pred out(u32).
7811            source(1).
7812            out(X) :- source(X).
7813            ?- out(X).
7814        "#;
7815        let program = LogicProgram::compile(source)?;
7816        let foreign_program = LogicProgram::compile(source)?;
7817        let Some(provider) = ground_term_encoding_test_provider() else {
7818            return Ok(());
7819        };
7820        let mut base_store = program.create_relation_store(provider.clone())?;
7821        let (_, initial_cache) =
7822            program.evaluate_with_relation_store_and_cache(provider.clone(), &base_store, false)?;
7823        let mut cache = Some(initial_cache);
7824        let mut runtime = None;
7825        let cache_before = cache.as_ref().map(std::ptr::from_ref);
7826        let source_version_before = base_store.version("source");
7827        let mut raw_delta_store = program.create_relation_store(provider.clone())?;
7828        let raw_insert = raw_delta_store
7829            .remove("source")
7830            .expect("inline source fact must materialize a nonempty raw delta");
7831        let raw_deltas = HashMap::from([(
7832            "source".to_string(),
7833            RelationDelta::new(Some(raw_insert), None),
7834        )]);
7835        let allocations_before_raw = provider.memory().alloc_count();
7836
7837        let error = match foreign_program.prepare_relation_deltas_commit_with_session_runtime(
7838            provider.clone(),
7839            &mut base_store,
7840            &mut cache,
7841            &mut runtime,
7842            raw_deltas,
7843        ) {
7844            Ok(_) => panic!("raw delta preparation must reject a foreign cache"),
7845            Err(error) => error,
7846        };
7847        assert_eq!(
7848            error.to_string(),
7849            "Execution error: materialized cache belongs to a different compiled logic program"
7850        );
7851        assert_eq!(provider.memory().alloc_count(), allocations_before_raw);
7852        assert_eq!(cache.as_ref().map(std::ptr::from_ref), cache_before);
7853        assert!(runtime.is_none());
7854        assert_eq!(base_store.version("source"), source_version_before);
7855
7856        let mut no_cache = None;
7857        let mut foreign_runtime =
7858            Some(program.create_session_runtime(provider.clone(), &base_store, false)?);
7859        let runtime_before = foreign_runtime.as_ref().map(std::ptr::from_ref);
7860        let mut prepared_delta_store = program.create_relation_store(provider.clone())?;
7861        let prepared_insert = prepared_delta_store
7862            .remove("source")
7863            .expect("inline source fact must materialize a nonempty prepared delta");
7864        let prepared_batch = program.prepare_relation_delta_batch(
7865            provider.as_ref(),
7866            vec![(
7867                "source".to_string(),
7868                RelationDelta::new(Some(prepared_insert), None),
7869            )],
7870            &BTreeSet::new(),
7871        )?;
7872        let allocations_before_prepared = provider.memory().alloc_count();
7873        let error = match foreign_program.prepare_relation_delta_commit_with_session_runtime(
7874            provider.clone(),
7875            &mut base_store,
7876            &mut no_cache,
7877            &mut foreign_runtime,
7878            prepared_batch,
7879        ) {
7880            Ok(_) => panic!("prepared delta commit must reject a foreign runtime"),
7881            Err(error) => error,
7882        };
7883        assert_eq!(
7884            error.to_string(),
7885            "Execution error: session runtime belongs to a different compiled logic program"
7886        );
7887        assert_eq!(provider.memory().alloc_count(), allocations_before_prepared);
7888        assert!(no_cache.is_none());
7889        assert_eq!(
7890            foreign_runtime.as_ref().map(std::ptr::from_ref),
7891            runtime_before
7892        );
7893        assert_eq!(base_store.version("source"), source_version_before);
7894
7895        let mut ordered_delta_store = program.create_relation_store(provider.clone())?;
7896        let ordered_insert = ordered_delta_store
7897            .remove("source")
7898            .expect("inline source fact must materialize a nonempty ordered delta");
7899        let allocations_before_ordered = provider.memory().alloc_count();
7900        let error = match foreign_program.apply_relation_delta_batch(
7901            provider.clone(),
7902            &mut base_store,
7903            &mut cache,
7904            vec![(
7905                "source".to_string(),
7906                RelationDelta::new(Some(ordered_insert), None),
7907            )],
7908        ) {
7909            Ok(_) => panic!("ordered delta application must reject a foreign cache"),
7910            Err(error) => error,
7911        };
7912        assert_eq!(
7913            error.to_string(),
7914            "Execution error: materialized cache belongs to a different compiled logic program"
7915        );
7916        assert_eq!(
7917            provider.memory().alloc_count(),
7918            allocations_before_ordered,
7919            "identity validation must run before ordered device coalescing"
7920        );
7921        assert_eq!(cache.as_ref().map(std::ptr::from_ref), cache_before);
7922        assert_eq!(base_store.version("source"), source_version_before);
7923        Ok(())
7924    }
7925
7926    #[test]
7927    fn persistent_relation_store_rejects_generated_query_head_before_setup() -> Result<()> {
7928        let program = LogicProgram::compile(
7929            r#"
7930                pred source(symbol).
7931                ?- source(X).
7932            "#,
7933        )?;
7934        let Some(provider) = ground_term_encoding_test_provider() else {
7935            return Ok(());
7936        };
7937        let mut store = program.create_relation_store(provider.clone())?;
7938        let query_schema = program
7939            .schemas
7940            .get("__xlog_query_0")
7941            .expect("compiler-generated query schema")
7942            .clone();
7943        store.put(
7944            "__xlog_query_0",
7945            provider.create_empty_buffer(query_schema)?,
7946        );
7947        let mut store_before = store
7948            .names()
7949            .map(|name| {
7950                (
7951                    name.to_string(),
7952                    store.get(name).expect("named relation").num_rows(),
7953                )
7954            })
7955            .collect::<Vec<_>>();
7956        store_before.sort_unstable();
7957        let allocations_before = provider.memory().alloc_count();
7958
7959        let error = match program.evaluate_with_relation_store(provider.clone(), &store, false) {
7960            Ok(_) => panic!("persistent caller must not seed a generated query head"),
7961            Err(error) => error,
7962        };
7963        assert!(matches!(error, XlogError::Execution(_)));
7964        assert!(error.to_string().contains(
7965            "persistent caller relation __xlog_query_0 collides with generated query head"
7966        ));
7967        assert_eq!(provider.memory().alloc_count(), allocations_before);
7968        let mut store_after = store
7969            .names()
7970            .map(|name| {
7971                (
7972                    name.to_string(),
7973                    store.get(name).expect("named relation").num_rows(),
7974                )
7975            })
7976            .collect::<Vec<_>>();
7977        store_after.sort_unstable();
7978        assert_eq!(store_after, store_before);
7979        Ok(())
7980    }
7981
7982    #[test]
7983    fn persistent_session_rejects_generated_query_head_before_setup() -> Result<()> {
7984        let program = LogicProgram::compile(
7985            r#"
7986                pred source(symbol).
7987                ?- source(X).
7988            "#,
7989        )?;
7990        let Some(provider) = ground_term_encoding_test_provider() else {
7991            return Ok(());
7992        };
7993        let mut store = program.create_relation_store(provider.clone())?;
7994        let query_schema = program
7995            .schemas
7996            .get("__xlog_query_0")
7997            .expect("compiler-generated query schema")
7998            .clone();
7999        store.put(
8000            "__xlog_query_0",
8001            provider.create_empty_buffer(query_schema)?,
8002        );
8003        let mut store_before = store
8004            .names()
8005            .map(|name| {
8006                (
8007                    name.to_string(),
8008                    store.get(name).expect("named relation").num_rows(),
8009                )
8010            })
8011            .collect::<Vec<_>>();
8012        store_before.sort_unstable();
8013        let allocations_before = provider.memory().alloc_count();
8014
8015        let error = match program.create_session_runtime(provider.clone(), &store, false) {
8016            Ok(_) => panic!("persistent session must not seed a generated query head"),
8017            Err(error) => error,
8018        };
8019        assert!(matches!(error, XlogError::Execution(_)));
8020        assert!(error.to_string().contains(
8021            "persistent caller relation __xlog_query_0 collides with generated query head"
8022        ));
8023        assert_eq!(provider.memory().alloc_count(), allocations_before);
8024        let mut store_after = store
8025            .names()
8026            .map(|name| {
8027                (
8028                    name.to_string(),
8029                    store.get(name).expect("named relation").num_rows(),
8030                )
8031            })
8032            .collect::<Vec<_>>();
8033        store_after.sort_unstable();
8034        assert_eq!(store_after, store_before);
8035        Ok(())
8036    }
8037
8038    #[test]
8039    fn raw_delta_preparation_rejects_generated_query_head_without_consuming_state() -> Result<()> {
8040        let program = LogicProgram::compile(
8041            r#"
8042                pred source(symbol).
8043                ?- source(X).
8044            "#,
8045        )?;
8046        let Some(provider) = ground_term_encoding_test_provider() else {
8047            return Ok(());
8048        };
8049        let mut store = program.create_relation_store(provider.clone())?;
8050        let (_, cached_store) =
8051            program.evaluate_with_relation_store_and_cache(provider.clone(), &store, false)?;
8052        let mut cached_store = Some(cached_store);
8053        let cached_store_before = cached_store.as_ref().map(std::ptr::from_ref);
8054        let mut session_runtime = None;
8055        let mut store_before = store
8056            .names()
8057            .map(|name| {
8058                (
8059                    name.to_string(),
8060                    store.get(name).expect("named relation").num_rows(),
8061                )
8062            })
8063            .collect::<Vec<_>>();
8064        store_before.sort_unstable();
8065        let allocations_before = provider.memory().alloc_count();
8066        let deltas =
8067            HashMap::from([("__xlog_query_0".to_string(), RelationDelta::new(None, None))]);
8068
8069        let error = match program.prepare_relation_deltas_commit_with_session_runtime(
8070            provider.clone(),
8071            &mut store,
8072            &mut cached_store,
8073            &mut session_runtime,
8074            deltas,
8075        ) {
8076            Ok(_) => panic!("delta preparation must reject a generated query head"),
8077            Err(error) => error,
8078        };
8079        assert!(matches!(error, XlogError::Execution(_)));
8080        assert!(error
8081            .to_string()
8082            .contains("caller delta relation __xlog_query_0 collides with generated query head"));
8083        assert_eq!(provider.memory().alloc_count(), allocations_before);
8084        assert_eq!(
8085            cached_store.as_ref().map(std::ptr::from_ref),
8086            cached_store_before
8087        );
8088        assert!(session_runtime.is_none());
8089        let mut store_after = store
8090            .names()
8091            .map(|name| {
8092                (
8093                    name.to_string(),
8094                    store.get(name).expect("named relation").num_rows(),
8095                )
8096            })
8097            .collect::<Vec<_>>();
8098        store_after.sort_unstable();
8099        assert_eq!(store_after, store_before);
8100        Ok(())
8101    }
8102
8103    #[test]
8104    fn ordered_delta_preparation_rejects_generated_query_head_before_device_work() -> Result<()> {
8105        let program = LogicProgram::compile(
8106            r#"
8107                pred source(symbol).
8108                source("payload").
8109                ?- source(X).
8110            "#,
8111        )?;
8112        let Some(provider) = ground_term_encoding_test_provider() else {
8113            return Ok(());
8114        };
8115        let mut insert_store = program.create_relation_store(provider.clone())?;
8116        let insert = insert_store
8117            .remove("source")
8118            .expect("inline source fact must materialize a nonempty insert");
8119        let mut delete_store = program.create_relation_store(provider.clone())?;
8120        let delete = delete_store
8121            .remove("source")
8122            .expect("inline source fact must materialize a nonempty delete");
8123        let allocations_before = provider.memory().alloc_count();
8124
8125        let error = match program.prepare_relation_delta_batch(
8126            provider.as_ref(),
8127            vec![
8128                (
8129                    "__xlog_query_0".to_string(),
8130                    RelationDelta::new(Some(insert), None),
8131                ),
8132                (
8133                    "__xlog_query_0".to_string(),
8134                    RelationDelta::new(None, Some(delete)),
8135                ),
8136            ],
8137            &BTreeSet::from(["__xlog_query_0".to_string()]),
8138        ) {
8139            Ok(_) => panic!("ordered delta preparation must reject a generated query head"),
8140            Err(error) => error,
8141        };
8142        assert!(matches!(error, XlogError::Execution(_)));
8143        assert!(error
8144            .to_string()
8145            .contains("caller delta relation __xlog_query_0 collides with generated query head"));
8146        assert_eq!(provider.memory().alloc_count(), allocations_before);
8147        Ok(())
8148    }
8149
8150    #[test]
8151    fn resident_rejects_caller_input_for_exact_generated_query_head_before_setup() -> Result<()> {
8152        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8153        let program = LogicProgram::compile(
8154            r#"
8155                pred source(symbol).
8156                ?- source(X).
8157            "#,
8158        )?;
8159        let Some(provider) = ground_term_encoding_test_provider() else {
8160            return Ok(());
8161        };
8162        let generated_schema = program
8163            .schemas
8164            .get("__xlog_query_0")
8165            .expect("generated query schema")
8166            .clone();
8167        for (name, value) in [
8168            ("XLOG_DISABLE_RESIDENT_RECURSION", "1"),
8169            ("XLOG_USE_RESIDENT_RECURSION", "1"),
8170            ("XLOG_REQUIRE_RESIDENT_RECURSION", "1"),
8171        ] {
8172            let input = provider.create_empty_buffer(generated_schema.clone())?;
8173            let allocations_before = provider.memory().alloc_count();
8174            let _env = ResidentEnvGuard::set(&[(name, value)]);
8175            let error = match program.evaluate_with_options(
8176                provider.clone(),
8177                HashMap::from([("__xlog_query_0".to_string(), input)]),
8178                true,
8179            ) {
8180                Ok(_) => panic!("caller input must not occupy a generated query head"),
8181                Err(error) => error,
8182            };
8183            assert!(
8184                error.to_string().contains(
8185                    "caller input relation __xlog_query_0 collides with generated query head"
8186                ),
8187                "unexpected caller-input collision error: {error}"
8188            );
8189            assert_eq!(
8190                provider.memory().alloc_count(),
8191                allocations_before,
8192                "generated-head collision must fail before resident setup"
8193            );
8194        }
8195        Ok(())
8196    }
8197
8198    #[test]
8199    fn resident_executor_distinguishes_derived_placeholders_from_explicit_empty_inputs(
8200    ) -> Result<()> {
8201        let program = LogicProgram::compile(
8202            r#"
8203                pred source(symbol).
8204                pred answer(symbol, symbol).
8205                pred seeded_derived(symbol).
8206                seeded_derived("seed").
8207                seeded_derived(X) :- source(X).
8208                answer("yes", X) :- source(X).
8209                ?- answer(Outcome, Claim).
8210            "#,
8211        )?;
8212        let LogicExecutionPlan::Ordinary(plan) = &program.plan else {
8213            panic!("ordinary test program must compile to an ordinary plan");
8214        };
8215        let Some(provider) = ground_term_encoding_test_provider() else {
8216            return Ok(());
8217        };
8218
8219        let compiler_seeded =
8220            program.prepare_resident_executor(&provider, HashMap::new(), false, plan)?;
8221        assert!(compiler_seeded.store().get("source").is_some());
8222        assert!(compiler_seeded.store().get("answer").is_none());
8223        assert_eq!(
8224            compiler_seeded
8225                .store()
8226                .get("seeded_derived")
8227                .expect("derived relation with inline facts must be seeded")
8228                .cached_row_count(),
8229            Some(1),
8230        );
8231
8232        let explicit_schema = Schema::new(vec![
8233            ("external_outcome".into(), ScalarType::Symbol),
8234            ("external_claim".into(), ScalarType::Symbol),
8235        ]);
8236        let explicit_empty = provider.create_empty_buffer(explicit_schema.clone())?;
8237        let explicitly_seeded = program.prepare_resident_executor(
8238            &provider,
8239            HashMap::from([("answer".to_string(), explicit_empty)]),
8240            false,
8241            plan,
8242        )?;
8243        assert_eq!(
8244            explicitly_seeded
8245                .store()
8246                .get("answer")
8247                .expect("explicit empty input must be retained")
8248                .schema(),
8249            &explicit_schema,
8250        );
8251        Ok(())
8252    }
8253
8254    #[test]
8255    fn required_resident_query_schema_matches_legacy_for_nonempty_result() -> Result<()> {
8256        let expected_schema = Schema::new(vec![
8257            ("computed_0".into(), ScalarType::Symbol),
8258            ("c0".into(), ScalarType::Symbol),
8259        ]);
8260        assert_required_resident_semantics(
8261            r#"
8262                pred source(symbol).
8263                pred answer(symbol, symbol).
8264                source("claim").
8265                answer("yes", X) :- source(X).
8266                ?- answer(Outcome, Claim).
8267            "#,
8268            "nonempty synthetic query schema",
8269            Some(&expected_schema),
8270            None,
8271            None,
8272        )
8273    }
8274
8275    #[test]
8276    fn required_resident_query_schema_matches_legacy_for_empty_result() -> Result<()> {
8277        let expected_schema = Schema::new(vec![
8278            ("computed_0".into(), ScalarType::Symbol),
8279            ("c0".into(), ScalarType::Symbol),
8280        ]);
8281        assert_required_resident_semantics(
8282            r#"
8283                pred source(symbol).
8284                pred answer(symbol, symbol).
8285                answer("yes", X) :- source(X).
8286                ?- answer(Outcome, Claim).
8287            "#,
8288            "empty synthetic query schema",
8289            Some(&expected_schema),
8290            None,
8291            None,
8292        )
8293    }
8294
8295    #[test]
8296    fn required_resident_recursive_query_schema_matches_legacy() -> Result<()> {
8297        let expected_schema = Schema::new(vec![("c0".into(), ScalarType::U32)]);
8298        assert_required_resident_semantics(
8299            r#"
8300                pred seed(u32).
8301                pred edge(u32, u32).
8302                pred reach(u32).
8303                seed(1).
8304                edge(1, 2).
8305                reach(X) :- seed(X).
8306                reach(X) :- reach(X), edge(X, Y).
8307                ?- reach(X).
8308            "#,
8309            "recursive synthetic query schema",
8310            Some(&expected_schema),
8311            None,
8312            None,
8313        )
8314    }
8315
8316    #[test]
8317    fn required_resident_recursive_constant_projection_schema_matches_legacy() -> Result<()> {
8318        let expected_schema = Schema::new(vec![
8319            ("item".into(), ScalarType::U32),
8320            ("computed_1".into(), ScalarType::U32),
8321        ]);
8322        assert_required_resident_semantics(
8323            r#"
8324                pred seed(item: u32).
8325                pred path(item: u32, category: u32).
8326                seed(7).
8327                path(X, 1) :- seed(X).
8328                path(X, 2) :- path(X, 1).
8329                ?- path(Item, Category).
8330            "#,
8331            "recursive constant-projection schema",
8332            Some(&expected_schema),
8333            Some(&[ScalarType::U32, ScalarType::U32]),
8334            Some(2),
8335        )
8336    }
8337
8338    #[test]
8339    #[ignore = "requires a serialized release-mode CUDA acceptance run"]
8340    fn resident_semantic_acceptance_matrix() -> Result<()> {
8341        let ordinary_cases = [
8342            (
8343                "recursion",
8344                r#"
8345                    pred edge(u32, u32).
8346                    pred reach(u32, u32).
8347                    edge(1, 2). edge(2, 3).
8348                    reach(X, Y) :- edge(X, Y).
8349                    reach(X, Z) :- reach(X, Y), edge(Y, Z).
8350                    ?- reach(X, Y).
8351                "#,
8352            ),
8353            (
8354                "negation",
8355                r#"
8356                    pred item(u32). pred blocked(u32). pred visible(u32).
8357                    item(1). item(2). blocked(2).
8358                    visible(X) :- item(X), not blocked(X).
8359                    ?- visible(X).
8360                "#,
8361            ),
8362            (
8363                "constraint",
8364                r#"
8365                    pred safe(u32).
8366                    safe(1).
8367                    :- safe(2).
8368                    ?- safe(X).
8369                "#,
8370            ),
8371            (
8372                "multiple queries",
8373                r#"
8374                    pred seed(u32). pred left(u32). pred right(u32).
8375                    seed(7).
8376                    left(X) :- seed(X).
8377                    right(X) :- seed(X).
8378                    ?- left(X).
8379                    ?- right(X).
8380                "#,
8381            ),
8382            (
8383                "nullary set",
8384                r#"
8385                    pred ready(). pred answer().
8386                    ready().
8387                    answer() :- ready().
8388                    ?- answer().
8389                "#,
8390            ),
8391            (
8392                "same name with multiple arities",
8393                r#"
8394                    pred item(u32). pred item(u32, u32).
8395                    pred unary(u32). pred binary(u32, u32).
8396                    item(1). item(1, 2).
8397                    unary(X) :- item(X).
8398                    binary(X, Y) :- item(X, Y).
8399                    ?- unary(X).
8400                    ?- binary(X, Y).
8401                "#,
8402            ),
8403        ];
8404        for (case, source) in ordinary_cases {
8405            assert_required_resident_semantics(source, case, None, None, None)?;
8406        }
8407
8408        let output_cases: [(&str, &str, &[ScalarType], usize); 7] = [
8409            (
8410                "zero-row output",
8411                r#"
8412                    pred empty(u32).
8413                    ?- empty(X).
8414                "#,
8415                &[ScalarType::U32],
8416                0,
8417            ),
8418            (
8419                "identity output",
8420                r#"
8421                    pred source(u32). pred copied(u32).
8422                    source(2). source(1).
8423                    copied(X) :- source(X).
8424                    ?- copied(X).
8425                "#,
8426                &[ScalarType::U32],
8427                2,
8428            ),
8429            (
8430                "u64 output",
8431                r#"
8432                    pred wide(u64).
8433                    wide(5000000000).
8434                    ?- wide(X).
8435                "#,
8436                &[ScalarType::U64],
8437                1,
8438            ),
8439            (
8440                "symbol output",
8441                r#"
8442                    pred labeled(symbol).
8443                    labeled("claim").
8444                    ?- labeled(X).
8445                "#,
8446                &[ScalarType::Symbol],
8447                1,
8448            ),
8449            (
8450                "mixed output",
8451                r#"
8452                    pred mixed(u32, u64, symbol).
8453                    mixed(7, 5000000000, "claim").
8454                    ?- mixed(Small, Wide, Label).
8455                "#,
8456                &[ScalarType::U32, ScalarType::U64, ScalarType::Symbol],
8457                1,
8458            ),
8459            (
8460                "nullary output",
8461                r#"
8462                    pred ready(). pred answer().
8463                    ready().
8464                    answer() :- ready().
8465                    ?- answer().
8466                "#,
8467                &[],
8468                1,
8469            ),
8470            (
8471                "arity-seventeen output",
8472                r#"
8473                    pred wide(u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32).
8474                    wide(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17).
8475                    ?- wide(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q).
8476                "#,
8477                &[ScalarType::U32; 17],
8478                1,
8479            ),
8480        ];
8481        for (case, source, expected_types, expected_rows) in output_cases {
8482            assert_required_resident_semantics(
8483                source,
8484                case,
8485                None,
8486                Some(expected_types),
8487                Some(expected_rows),
8488            )?;
8489        }
8490
8491        assert!(
8492            LogicProgram::compile(
8493                r#"
8494                    pred seed(u32). pred invalid(u32).
8495                    seed(1).
8496                    invalid(X) :- seed(X), X = "wrong type".
8497                    ?- seed(X).
8498                "#,
8499            )
8500            .is_err(),
8501            "invalid unreachable rules must still be validated"
8502        );
8503
8504        required_resident_evaluation_canonicalizes_a_caller_input_only_relation()?;
8505        complete_store_evaluation_declines_resident_partial_execution_and_materializes_all_heads()?;
8506
8507        {
8508            let _env_lock = resident_env_lock().lock().expect("resident env lock");
8509            let Some(provider) = ground_term_encoding_test_provider() else {
8510                return Ok(());
8511            };
8512            let program = LogicProgram::compile(
8513                r#"
8514                    pred seed(u32). pred out(u32).
8515                    seed(1). out(X) :- seed(X). ?- out(X).
8516                "#,
8517            )?;
8518            let store = program.create_relation_store(provider.clone())?;
8519            let mut session = program.create_session_runtime(provider.clone(), &store, true)?;
8520            let _env = ResidentEnvGuard::set(&[("XLOG_USE_RESIDENT_RECURSION", "1")]);
8521            let (result, _) = program.evaluate_with_session_runtime(provider, &mut session)?;
8522            let graph = result
8523                .stats
8524                .as_ref()
8525                .and_then(|stats| stats.resident_graph.as_ref())
8526                .expect("session resident decline telemetry");
8527            assert_eq!(graph.selection, ResidentGraphSelectionKind::ExistingGpu);
8528            assert_eq!(
8529                graph.decline,
8530                Some(ResidentGraphDeclineReason::FullStoreRequested)
8531            );
8532            assert_eq!(graph.conditional_graph_launches, 0);
8533        }
8534
8535        {
8536            let _env_lock = resident_env_lock().lock().expect("resident env lock");
8537            let Some(provider) = ground_term_encoding_test_provider() else {
8538                return Ok(());
8539            };
8540            let program = LogicProgram::compile(
8541                r#"
8542                    pred p(u32). pred q(u32).
8543                    p(1). q(X) :- p(X), know p(X). ?- q(X).
8544                "#,
8545            )?;
8546            let baseline = {
8547                let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
8548                program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
8549            };
8550            let expected = snapshot_query_results(provider.as_ref(), &baseline)?;
8551            drop(baseline);
8552            let preferred = {
8553                let _env = ResidentEnvGuard::set(&[("XLOG_USE_RESIDENT_RECURSION", "1")]);
8554                program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
8555            };
8556            assert_eq!(
8557                snapshot_query_results(provider.as_ref(), &preferred)?,
8558                expected
8559            );
8560            let graph = preferred
8561                .stats
8562                .as_ref()
8563                .and_then(|stats| stats.resident_graph.as_ref())
8564                .expect("nonordinary resident decline telemetry");
8565            assert_eq!(graph.selection, ResidentGraphSelectionKind::ExistingGpu);
8566            assert_eq!(
8567                graph.decline,
8568                Some(ResidentGraphDeclineReason::NonOrdinaryPlan)
8569            );
8570            assert_eq!(graph.conditional_graph_launches, 0);
8571        }
8572        Ok(())
8573    }
8574
8575    #[test]
8576    fn complete_store_evaluation_declines_resident_partial_execution_and_materializes_all_heads(
8577    ) -> Result<()> {
8578        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8579        let Some(provider) = ground_term_encoding_test_provider() else {
8580            return Ok(());
8581        };
8582        let program = LogicProgram::compile(
8583            r#"
8584                pred base(u32).
8585                pred edge(u32, u32).
8586                pred queried(u32).
8587                pred disconnected(u32).
8588                base(7).
8589                edge(7, 8).
8590                queried(X) :- base(X).
8591                queried(Y) :- queried(X), edge(X, Y).
8592                disconnected(X) :- base(X).
8593                ?- queried(X).
8594            "#,
8595        )?;
8596        let seed = program.create_relation_store(provider.clone())?;
8597        let _env = ResidentEnvGuard::set(&[("XLOG_USE_RESIDENT_RECURSION", "1")]);
8598        let (result, store) =
8599            program.evaluate_with_relation_store_and_cache(provider.clone(), &seed, true)?;
8600
8601        assert_eq!(
8602            provider.download_column::<u32>(&result.queries[0].buffer, 0)?,
8603            vec![7, 8]
8604        );
8605        assert_eq!(
8606            provider.download_column::<u32>(
8607                store
8608                    .as_relation_store()
8609                    .get("disconnected")
8610                    .expect("complete disconnected head"),
8611                0,
8612            )?,
8613            vec![7]
8614        );
8615        let graph = result
8616            .stats
8617            .as_ref()
8618            .and_then(|stats| stats.resident_graph.as_ref())
8619            .expect("resident decline telemetry");
8620        assert_eq!(graph.selection, ResidentGraphSelectionKind::ExistingGpu);
8621        assert_eq!(
8622            graph.decline,
8623            Some(ResidentGraphDeclineReason::FullStoreRequested)
8624        );
8625        assert_eq!(graph.conditional_graph_launches, 0);
8626        assert_eq!(graph.staged_store_mutations, 0);
8627        assert!(result
8628            .stats
8629            .as_ref()
8630            .expect("profile")
8631            .format_json()
8632            .contains("resident_graph_declined"));
8633        Ok(())
8634    }
8635
8636    #[test]
8637    fn no_query_program_bypasses_resident_certification_and_executes_the_full_plan() -> Result<()> {
8638        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8639        let program = LogicProgram::compile(
8640            r#"
8641                pred seed(u32).
8642                pred disconnected(u32).
8643                seed(1).
8644                disconnected(X) :- seed(X).
8645            "#,
8646        )?;
8647        assert_eq!(program.resident_certification_initializations(), 0);
8648        let Some(provider) = ground_term_encoding_test_provider() else {
8649            return Ok(());
8650        };
8651
8652        let baseline = {
8653            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
8654            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
8655        };
8656        assert!(baseline.queries.is_empty());
8657        let baseline_profile =
8658            strata_op_profile(baseline.stats.as_ref().expect("baseline profile"));
8659        assert_eq!(op_count(baseline.stats.as_ref().unwrap(), "scan"), 1);
8660        assert_eq!(program.resident_certification_initializations(), 0);
8661        drop(baseline);
8662
8663        let preferred = {
8664            let _env = ResidentEnvGuard::set(&[("XLOG_USE_RESIDENT_RECURSION", "1")]);
8665            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
8666        };
8667        assert!(preferred.queries.is_empty());
8668        assert_eq!(
8669            strata_op_profile(preferred.stats.as_ref().expect("preferred profile")),
8670            baseline_profile
8671        );
8672        let graph = preferred
8673            .stats
8674            .as_ref()
8675            .and_then(|stats| stats.resident_graph.as_ref())
8676            .expect("resident decline telemetry");
8677        assert_eq!(graph.selection, ResidentGraphSelectionKind::ExistingGpu);
8678        assert_eq!(
8679            graph.decline,
8680            Some(ResidentGraphDeclineReason::FullStoreRequested)
8681        );
8682        assert_eq!(graph.conditional_graph_launches, 0);
8683        assert_eq!(program.resident_certification_initializations(), 0);
8684        drop(preferred);
8685
8686        let required = match {
8687            let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
8688            program.evaluate_with_options(provider, HashMap::new(), true)
8689        } {
8690            Ok(_) => panic!("no-query evaluation cannot use the resident partial-result route"),
8691            Err(error) => error,
8692        };
8693        assert!(required
8694            .to_string()
8695            .contains("resident conditional-graph execution was required but declined"));
8696        assert!(required.to_string().contains("FullStoreRequested"));
8697        assert_eq!(program.resident_certification_initializations(), 0);
8698        Ok(())
8699    }
8700
8701    #[test]
8702    fn preferred_resident_decline_executes_the_untouched_full_plan() -> Result<()> {
8703        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8704        let Some(provider) = ground_term_encoding_test_provider() else {
8705            return Ok(());
8706        };
8707        let program = LogicProgram::compile(
8708            r#"
8709                pred unsupported(f64).
8710                pred seed(u32).
8711                pred disconnected(u32).
8712                unsupported(7.5).
8713                seed(1).
8714                disconnected(X) :- seed(X).
8715                ?- unsupported(X).
8716            "#,
8717        )?;
8718        let baseline = {
8719            let _env = ResidentEnvGuard::set(&[("XLOG_DISABLE_RESIDENT_RECURSION", "1")]);
8720            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
8721        };
8722        let baseline_snapshot = snapshot_query_results(provider.as_ref(), &baseline)?;
8723        let baseline_profile =
8724            strata_op_profile(baseline.stats.as_ref().expect("baseline profile"));
8725        assert_eq!(op_count(baseline.stats.as_ref().unwrap(), "scan"), 2);
8726        drop(baseline);
8727
8728        let preferred = {
8729            let _env = ResidentEnvGuard::set(&[("XLOG_USE_RESIDENT_RECURSION", "1")]);
8730            program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
8731        };
8732        assert_eq!(
8733            snapshot_query_results(provider.as_ref(), &preferred)?,
8734            baseline_snapshot
8735        );
8736        assert_eq!(
8737            strata_op_profile(preferred.stats.as_ref().expect("preferred profile")),
8738            baseline_profile
8739        );
8740        let graph = preferred
8741            .stats
8742            .as_ref()
8743            .and_then(|stats| stats.resident_graph.as_ref())
8744            .expect("resident decline telemetry");
8745        assert_eq!(graph.selection, ResidentGraphSelectionKind::ExistingGpu);
8746        assert!(graph.decline.is_some());
8747        assert_eq!(graph.conditional_graph_launches, 0);
8748        Ok(())
8749    }
8750
8751    #[test]
8752    fn ordinary_unsupported_scalar_types_decline_before_launch() -> Result<()> {
8753        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8754        let Some(provider) = ground_term_encoding_test_provider() else {
8755            return Ok(());
8756        };
8757        let programs = [
8758            r#"
8759                pred unsupported(i64).
8760                unsupported(7).
8761                ?- unsupported(X).
8762            "#,
8763            r#"
8764                pred unsupported(f64).
8765                unsupported(7.5).
8766                ?- unsupported(X).
8767            "#,
8768            r#"
8769                pred unsupported(u32, f64).
8770                unsupported(7, 8.5).
8771                ?- unsupported(X, Y).
8772            "#,
8773        ];
8774        for source in programs {
8775            let program = LogicProgram::compile(source)?;
8776            let preferred = {
8777                let _env = ResidentEnvGuard::set(&[("XLOG_USE_RESIDENT_RECURSION", "1")]);
8778                program.evaluate_with_options(provider.clone(), HashMap::new(), true)?
8779            };
8780            assert_eq!(preferred.queries[0].buffer.cached_row_count(), Some(1));
8781            let graph = preferred
8782                .stats
8783                .as_ref()
8784                .and_then(|stats| stats.resident_graph.as_ref())
8785                .expect("resident decline telemetry");
8786            assert_eq!(graph.selection, ResidentGraphSelectionKind::ExistingGpu);
8787            assert!(graph.decline.is_some());
8788            assert_eq!(graph.conditional_graph_launches, 0);
8789            drop(preferred);
8790
8791            let error = {
8792                let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
8793                match program.evaluate_with_options(provider.clone(), HashMap::new(), true) {
8794                    Ok(_) => {
8795                        panic!("required resident execution must reject a prelaunch decline")
8796                    }
8797                    Err(error) => error,
8798                }
8799            };
8800            assert!(error
8801                .to_string()
8802                .contains("resident conditional-graph execution was required but declined"));
8803            assert_eq!(program.resident_certification_initializations(), 1);
8804        }
8805
8806        let mut cached_failure = LogicProgram::compile(
8807            r#"
8808                pred output(u32).
8809                output(7).
8810                ?- output(X).
8811            "#,
8812        )?;
8813        assert_eq!(cached_failure.resident_certification_initializations(), 1);
8814        cached_failure.reusable_state_identity = Arc::new(LogicProgramIdentity::new());
8815        cached_failure
8816            .reusable_state_identity
8817            .get_or_init_resident_certification(|| -> Result<ResidentGraphCertifiedPlan> {
8818                Err(XlogError::Execution(
8819                    "deterministic certification failure".into(),
8820                ))
8821            })
8822            .expect_err("injected certification must fail");
8823        let preferred = {
8824            let _env = ResidentEnvGuard::set(&[("XLOG_USE_RESIDENT_RECURSION", "1")]);
8825            cached_failure.evaluate_with_options(provider.clone(), HashMap::new(), true)?
8826        };
8827        assert_eq!(preferred.queries[0].buffer.cached_row_count(), Some(1));
8828        let decline = preferred
8829            .stats
8830            .as_ref()
8831            .and_then(|stats| stats.resident_graph.as_ref())
8832            .and_then(|stats| stats.decline.as_ref())
8833            .expect("preferred certification failure must report its decline");
8834        assert!(format!("{decline:?}").contains("deterministic certification failure"));
8835        drop(preferred);
8836
8837        let required = {
8838            let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
8839            match cached_failure.evaluate_with_options(provider, HashMap::new(), true) {
8840                Ok(_) => panic!("required resident execution must reject cached certification"),
8841                Err(error) => error,
8842            }
8843        };
8844        assert!(required
8845            .to_string()
8846            .contains("resident conditional-graph execution was required but declined"));
8847        assert!(required
8848            .to_string()
8849            .contains("deterministic certification failure"));
8850        assert_eq!(cached_failure.resident_certification_initializations(), 1);
8851        Ok(())
8852    }
8853
8854    #[test]
8855    fn ordinary_evaluation_uses_the_required_resident_graph_on_the_callers_cuda_context(
8856    ) -> Result<()> {
8857        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8858        let Some(provider) = ground_term_encoding_test_provider() else {
8859            return Ok(());
8860        };
8861        assert!(provider.memory().runtime().is_none());
8862        let program = LogicProgram::compile(
8863            r#"
8864                pred seed(u32).
8865                pred edge(u32, u32).
8866                pred reach(u32).
8867                seed(1).
8868                edge(1, 2).
8869                edge(2, 3).
8870                reach(X) :- seed(X).
8871                reach(Y) :- reach(X), edge(X, Y), Y >= 2.
8872                ?- reach(X).
8873            "#,
8874        )?;
8875        let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
8876        let result = program.evaluate_with_options(provider.clone(), HashMap::new(), true)?;
8877        assert_eq!(
8878            provider.download_column::<u32>(&result.queries[0].buffer, 0)?,
8879            vec![1, 2, 3]
8880        );
8881        let graph = result
8882            .stats
8883            .as_ref()
8884            .and_then(|stats| stats.resident_graph.as_ref())
8885            .expect("resident selection telemetry");
8886        assert_eq!(
8887            graph.selection,
8888            ResidentGraphSelectionKind::ResidentConditionalGraph
8889        );
8890        assert_eq!(graph.conditional_graph_launches, 1);
8891        assert!(graph.device_scan_invocations > 0);
8892        assert!(graph.device_filter_invocations > 0);
8893        assert_eq!(graph.core_transfers.tracked_htod_calls, 0);
8894        assert_eq!(graph.core_transfers.tracked_dtoh_calls, 0);
8895        assert_eq!(graph.core_transfers.provider_dtoh_calls, 0);
8896        assert_eq!(graph.core_transfers.untracked_metadata_dtoh_calls, 0);
8897        assert_eq!(graph.final_observation.dtoh_calls, 1);
8898        assert_eq!(graph.final_observation.pinned_receipts, 1);
8899        assert!(graph.deferred_profile.device_elapsed_ns > 0);
8900        Ok(())
8901    }
8902
8903    #[test]
8904    fn required_resident_evaluation_canonicalizes_a_caller_input_only_relation() -> Result<()> {
8905        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8906        let Some(provider) = ground_term_encoding_test_provider() else {
8907            return Ok(());
8908        };
8909        let program = LogicProgram::compile(
8910            r#"
8911                pred input(u32).
8912                pred output(u32).
8913                output(X) :- input(X).
8914                ?- output(X).
8915            "#,
8916        )?;
8917        let input = provider.create_buffer_from_slice::<u32>(
8918            &[2, 1, 2],
8919            Schema::new(vec![("x".into(), ScalarType::U32)]),
8920        )?;
8921        assert!(!input.canonical_full_row_set_certified());
8922        let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
8923        let result = program.evaluate_with_options(
8924            provider.clone(),
8925            HashMap::from([("input".to_string(), input)]),
8926            true,
8927        )?;
8928        assert_eq!(
8929            provider.download_column::<u32>(&result.queries[0].buffer, 0)?,
8930            vec![1, 2]
8931        );
8932        let graph = result
8933            .stats
8934            .as_ref()
8935            .and_then(|stats| stats.resident_graph.as_ref())
8936            .expect("resident selection telemetry");
8937        assert_eq!(
8938            graph.selection,
8939            ResidentGraphSelectionKind::ResidentConditionalGraph
8940        );
8941        assert_eq!(graph.conditional_graph_launches, 1);
8942        Ok(())
8943    }
8944
8945    #[test]
8946    fn incompatible_caller_input_type_fails_before_resident_allocation_or_launch() -> Result<()> {
8947        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8948        let Some(provider) = ground_term_encoding_test_provider() else {
8949            return Ok(());
8950        };
8951        let program = LogicProgram::compile(
8952            r#"
8953                pred input(u32).
8954                pred output(u32).
8955                output(X) :- input(X).
8956                ?- output(X).
8957            "#,
8958        )?;
8959        let input = provider.create_buffer_from_slice::<u64>(
8960            &[1],
8961            Schema::new(vec![("x".into(), ScalarType::U64)]),
8962        )?;
8963        provider.memory().reset_alloc_count();
8964        let allocated_before = provider.memory().allocated_bytes();
8965        let _env = ResidentEnvGuard::set(&[("XLOG_REQUIRE_RESIDENT_RECURSION", "1")]);
8966        let error = match program.evaluate_with_options(
8967            provider.clone(),
8968            HashMap::from([("input".to_string(), input)]),
8969            false,
8970        ) {
8971            Err(error) => error,
8972            Ok(_) => panic!("incompatible input type unexpectedly reached resident setup"),
8973        };
8974        assert!(error.to_string().contains("schema mismatch"));
8975        assert!(provider.memory().allocated_bytes() <= allocated_before);
8976        assert_eq!(provider.memory().alloc_count(), 0);
8977        assert!(provider.memory().runtime().is_none());
8978        Ok(())
8979    }
8980
8981    #[test]
8982    fn malformed_caller_device_count_fails_before_resident_allocation_or_launch() -> Result<()> {
8983        let _env_lock = resident_env_lock().lock().expect("resident env lock");
8984        let Some(provider) = ground_term_encoding_test_provider() else {
8985            return Ok(());
8986        };
8987        let program = LogicProgram::compile(
8988            r#"
8989                pred input(u32).
8990                pred output(u32).
8991                output(X) :- input(X).
8992                ?- output(X).
8993            "#,
8994        )?;
8995        for selection_env in [
8996            "XLOG_USE_RESIDENT_RECURSION",
8997            "XLOG_REQUIRE_RESIDENT_RECURSION",
8998        ] {
8999            let mut column = provider.memory().alloc::<u8>(4)?;
9000            provider
9001                .device()
9002                .inner()
9003                .htod_sync_copy_into(&7u32.to_ne_bytes(), &mut column)
9004                .map_err(|error| XlogError::Kernel(error.to_string()))?;
9005            let mut device_count = provider.memory().alloc::<u32>(1)?;
9006            provider
9007                .device()
9008                .inner()
9009                .htod_sync_copy_into(&[2], &mut device_count)
9010                .map_err(|error| XlogError::Kernel(error.to_string()))?;
9011            let input = CudaBuffer::from_columns(
9012                vec![column.into()],
9013                1,
9014                device_count,
9015                Schema::new(vec![("x".into(), ScalarType::U32)]),
9016            );
9017            provider.memory().reset_alloc_count();
9018            let allocated_before = provider.memory().allocated_bytes();
9019            let _env = ResidentEnvGuard::set(&[(selection_env, "1")]);
9020            let error = match program.evaluate_with_options(
9021                provider.clone(),
9022                HashMap::from([("input".to_string(), input)]),
9023                false,
9024            ) {
9025                Err(error) => error,
9026                Ok(_) => panic!("malformed input count unexpectedly reached resident setup"),
9027            };
9028            assert!(
9029                error
9030                    .to_string()
9031                    .contains("Logical row count 2 exceeds row capacity 1"),
9032                "unexpected error for {selection_env}: {error}"
9033            );
9034            assert!(provider.memory().allocated_bytes() <= allocated_before);
9035            assert_eq!(provider.memory().alloc_count(), 0);
9036            assert!(provider.memory().runtime().is_none());
9037        }
9038        Ok(())
9039    }
9040
9041    #[test]
9042    fn program_fact_loader_uses_shared_ground_term_encoding() -> Result<()> {
9043        let Some(provider) = ground_term_encoding_test_provider() else {
9044            return Ok(());
9045        };
9046        let program = LogicProgram::compile(
9047            r#"
9048                pred encoded(u32, u64, i32, i64, f32, f64, bool, bool, symbol, symbol).
9049                encoded(42, 43, -44, -45, 1.5, 2.25, true, 0, "hello", world).
9050            "#,
9051        )?;
9052        let store = program.create_relation_store(provider.clone())?;
9053        let encoded = store
9054            .get("encoded")
9055            .ok_or_else(|| XlogError::Execution("missing encoded fact buffer".to_string()))?;
9056
9057        assert_eq!(provider.download_column::<u32>(encoded, 0)?, vec![42]);
9058        assert_eq!(provider.download_column::<u64>(encoded, 1)?, vec![43]);
9059        assert_eq!(provider.download_column::<i32>(encoded, 2)?, vec![-44]);
9060        assert_eq!(provider.download_column::<i64>(encoded, 3)?, vec![-45]);
9061        assert_eq!(provider.download_column::<f32>(encoded, 4)?, vec![1.5]);
9062        assert_eq!(provider.download_column::<f64>(encoded, 5)?, vec![2.25]);
9063        assert_eq!(provider.download_column::<bool>(encoded, 6)?, vec![true]);
9064        assert_eq!(provider.download_column::<bool>(encoded, 7)?, vec![false]);
9065        assert_eq!(
9066            provider.download_column::<u32>(encoded, 8)?,
9067            vec![symbol::intern("hello")]
9068        );
9069        assert_eq!(
9070            provider.download_column::<u32>(encoded, 9)?,
9071            vec![symbol::intern("world")]
9072        );
9073
9074        let invalid = LogicProgram::compile(
9075            r#"
9076                pred invalid(u32).
9077                invalid(X).
9078            "#,
9079        )?;
9080        let error = match invalid.create_relation_store(provider) {
9081            Ok(_) => panic!("a variable in a fact must be rejected"),
9082            Err(error) => error,
9083        };
9084        let XlogError::Execution(message) = error else {
9085            panic!("fact encoding must remain an Execution error, got {error:?}");
9086        };
9087        assert_eq!(
9088            message,
9089            "Failed to encode fact for predicate invalid at column 0: Fact cannot contain variable X"
9090        );
9091        Ok(())
9092    }
9093
9094    #[test]
9095    fn grouped_facts_preserve_fact_and_rule_results_without_seed_operations() -> Result<()> {
9096        let Some(provider) = ground_term_encoding_test_provider() else {
9097            return Ok(());
9098        };
9099        let program = LogicProgram::compile(
9100            r#"
9101                pred base(u32).
9102                pred derived(u32).
9103                base(1).
9104                base(1).
9105                base(2).
9106                derived(X) :- base(X).
9107                ?- base(X).
9108                ?- derived(X).
9109            "#,
9110        )?;
9111        let LogicExecutionPlan::Ordinary(plan) = &program.plan else {
9112            panic!("ordinary source must compile to an ordinary plan");
9113        };
9114        let executable_rule_count = plan.rules_by_scc.iter().map(Vec::len).sum::<usize>();
9115        assert_eq!(program.program.facts().count(), 3);
9116        let expected_executable_rules =
9117            program.program.proper_rules().count() + program.program.queries.len();
9118        assert_eq!(
9119            executable_rule_count, expected_executable_rules,
9120            "compiled rules must correspond only to executable source and query rules"
9121        );
9122
9123        provider.reset_host_transfer_stats();
9124        let mut executor = program.prepare_executor(&provider, HashMap::new(), true)?;
9125        let fact_load_transfers = provider.host_transfer_stats();
9126        assert_eq!(fact_load_transfers.htod_calls, 1);
9127        assert_eq!(
9128            fact_load_transfers.htod_bytes,
9129            3 * std::mem::size_of::<u32>() as u64
9130        );
9131        assert_eq!(fact_load_transfers.dtoh_calls, 0);
9132        assert_eq!(fact_load_transfers.dtoh_bytes, 0);
9133        let base = executor
9134            .store()
9135            .get("base")
9136            .ok_or_else(|| XlogError::Execution("missing grouped base facts".to_string()))?;
9137        assert_eq!(base.cached_row_count(), Some(2));
9138        let mut materialized_base = provider.download_column::<u32>(base, 0)?;
9139        materialized_base.sort_unstable();
9140        assert_eq!(materialized_base, vec![1, 2]);
9141
9142        executor.execute_plan(plan)?;
9143        for query_index in 0..2 {
9144            let relation_name = format!("__xlog_query_{query_index}");
9145            let query = executor.store().get(&relation_name).ok_or_else(|| {
9146                XlogError::Execution(format!("missing query relation {relation_name}"))
9147            })?;
9148            let mut rows = provider.download_column::<u32>(query, 0)?;
9149            rows.sort_unstable();
9150            assert_eq!(rows, vec![1, 2]);
9151        }
9152
9153        let stats = executor.execution_stats(4);
9154        let scan_count = stats
9155            .strata
9156            .iter()
9157            .flat_map(|stratum| &stratum.ops)
9158            .filter(|op| op.op_name == "scan")
9159            .count();
9160        let union_count = stats
9161            .strata
9162            .iter()
9163            .flat_map(|stratum| &stratum.ops)
9164            .filter(|op| op.op_name == "union")
9165            .count();
9166        assert_eq!(scan_count, executable_rule_count);
9167        assert_eq!(union_count, executable_rule_count);
9168        Ok(())
9169    }
9170
9171    #[test]
9172    fn ordinary_execution_after_fact_setup_has_no_host_transfers() -> Result<()> {
9173        let Some(provider) = ground_term_encoding_test_provider() else {
9174            return Ok(());
9175        };
9176        let program = LogicProgram::compile(
9177            r#"
9178                pred base(u32).
9179                pred derived(u32).
9180                base(7).
9181                derived(X) :- base(X).
9182                ?- derived(X).
9183            "#,
9184        )?;
9185        let LogicExecutionPlan::Ordinary(plan) = &program.plan else {
9186            panic!("ordinary source must compile to an ordinary plan");
9187        };
9188
9189        let mut executor = program.prepare_executor(&provider, HashMap::new(), false)?;
9190        provider.reset_host_transfer_stats();
9191        provider.reset_d2h_transfer_count();
9192        provider.reset_untracked_metadata_dtoh_count();
9193        provider.reset_deterministic_d2h_violations();
9194        executor.execute_plan(plan)?;
9195
9196        let transfers = provider.host_transfer_stats();
9197        assert_eq!(
9198            transfers.htod_calls, 0,
9199            "execution must not upload host data"
9200        );
9201        assert_eq!(
9202            transfers.htod_bytes, 0,
9203            "execution must not upload host bytes"
9204        );
9205        assert_eq!(
9206            transfers.dtoh_calls, 0,
9207            "execution must not download device data"
9208        );
9209        assert_eq!(
9210            transfers.dtoh_bytes, 0,
9211            "execution must not download device bytes"
9212        );
9213        assert_eq!(provider.d2h_transfer_count(), 0);
9214        assert_eq!(provider.untracked_metadata_dtoh_count(), 0);
9215        assert_eq!(provider.deterministic_d2h_violation_count(), 0);
9216
9217        let query = executor
9218            .store()
9219            .get("__xlog_query_0")
9220            .ok_or_else(|| XlogError::Execution("missing query result".to_string()))?;
9221        assert_eq!(provider.download_column::<u32>(query, 0)?, vec![7]);
9222        Ok(())
9223    }
9224
9225    #[test]
9226    fn grouped_fact_loading_preserves_arity_qualified_relations() -> Result<()> {
9227        let Some(provider) = ground_term_encoding_test_provider() else {
9228            return Ok(());
9229        };
9230        let program = LogicProgram::compile(
9231            r#"
9232                #pragma epistemic_mode = faeel
9233                pred node(symbol).
9234                pred source(symbol, i64).
9235                pred source(u32).
9236                pred result(symbol).
9237                node(key).
9238                source(key, 5000000000).
9239                source(key, 5000000000).
9240                source(1).
9241                source(1).
9242                result(X) :- node(X), know source(X, Y).
9243                ?- result(X).
9244            "#,
9245        )?;
9246        let store = program.create_relation_store(provider.clone())?;
9247        let unary = store
9248            .get("source/1")
9249            .ok_or_else(|| XlogError::Execution("missing unary source facts".to_string()))?;
9250        let binary = store
9251            .get("source/2")
9252            .ok_or_else(|| XlogError::Execution("missing binary source facts".to_string()))?;
9253
9254        assert_eq!(unary.cached_row_count(), Some(1));
9255        assert_eq!(binary.cached_row_count(), Some(1));
9256        assert_eq!(provider.download_column::<u32>(unary, 0)?, vec![1]);
9257        assert_eq!(
9258            provider.download_column::<u32>(binary, 0)?,
9259            vec![symbol::intern("key")]
9260        );
9261        assert_eq!(
9262            provider.download_column::<i64>(binary, 1)?,
9263            vec![5_000_000_000]
9264        );
9265
9266        let evidence = program.execute_epistemic_evidence(provider.clone(), HashMap::new())?;
9267        assert_eq!(
9268            evidence.final_output.schema().arity(),
9269            1,
9270            "the epistemic evidence path must project the public result arity"
9271        );
9272        assert_eq!(
9273            provider.download_column::<u32>(&evidence.final_output, 0)?,
9274            vec![symbol::intern("key")],
9275            "the epistemic evidence path must execute the binary modal source facts"
9276        );
9277        Ok(())
9278    }
9279
9280    #[test]
9281    fn ordinary_compile_qualifies_same_name_multi_arity_relations() -> Result<()> {
9282        let program = LogicProgram::compile(
9283            r#"
9284                pred item(u32). pred item(u32, u32).
9285                pred unary(u32). pred binary(u32, u32).
9286                item(1). item(1, 2).
9287                unary(X) :- item(X).
9288                binary(X, Y) :- item(X, Y).
9289                ?- unary(X).
9290                ?- binary(X, Y).
9291            "#,
9292        )?;
9293
9294        let unary_item = *program
9295            .rel_ids
9296            .get("item/1")
9297            .expect("ordinary compiler registers item/1");
9298        let binary_item = *program
9299            .rel_ids
9300            .get("item/2")
9301            .expect("ordinary compiler registers item/2");
9302        assert_ne!(unary_item, binary_item);
9303
9304        let LogicExecutionPlan::Ordinary(plan) = &program.plan else {
9305            panic!("ordinary source must compile to an ordinary execution plan");
9306        };
9307        let unary_rule = plan
9308            .rules_by_scc
9309            .iter()
9310            .flatten()
9311            .find(|rule| rule.head == "unary")
9312            .expect("compiled unary rule");
9313        let binary_rule = plan
9314            .rules_by_scc
9315            .iter()
9316            .flatten()
9317            .find(|rule| rule.head == "binary")
9318            .expect("compiled binary rule");
9319        assert_eq!(unary_rule.body.referenced_relations(), vec![unary_item]);
9320        assert_eq!(binary_rule.body.referenced_relations(), vec![binary_item]);
9321        Ok(())
9322    }
9323
9324    #[test]
9325    fn recursive_execution_preserves_inline_fact_semantics() -> Result<()> {
9326        let Some(provider) = ground_term_encoding_test_provider() else {
9327            return Ok(());
9328        };
9329        let program = LogicProgram::compile(
9330            r#"
9331                pred edge(u32, u32).
9332                pred reach(u32, u32).
9333                edge(1, 2).
9334                edge(2, 3).
9335                reach(X, Y) :- edge(X, Y).
9336                reach(X, Z) :- reach(X, Y), edge(Y, Z).
9337                ?- reach(X, Z).
9338            "#,
9339        )?;
9340
9341        let result = program.evaluate(provider.clone(), HashMap::new())?;
9342        assert_eq!(result.queries.len(), 1);
9343        let xs = provider.download_column::<u32>(&result.queries[0].buffer, 0)?;
9344        let zs = provider.download_column::<u32>(&result.queries[0].buffer, 1)?;
9345        let mut rows = xs.into_iter().zip(zs).collect::<Vec<_>>();
9346        rows.sort_unstable();
9347        assert_eq!(
9348            rows,
9349            vec![(1, 2), (1, 3), (2, 3)],
9350            "recursive production execution must derive the transitive inline-fact result"
9351        );
9352        Ok(())
9353    }
9354
9355    #[test]
9356    fn nullary_execution_preserves_asserted_inline_fact_truth() -> Result<()> {
9357        let Some(provider) = ground_term_encoding_test_provider() else {
9358            return Ok(());
9359        };
9360        let program = LogicProgram::compile(
9361            r#"
9362                pred enabled().
9363                pred result().
9364                enabled().
9365                result() :- enabled().
9366                ?- result().
9367            "#,
9368        )?;
9369
9370        let result = program.evaluate(provider.clone(), HashMap::new())?;
9371        assert_eq!(result.queries.len(), 1);
9372        assert_eq!(
9373            provider.device_row_count(&result.queries[0].buffer)?,
9374            1,
9375            "an asserted nullary fact must make the derived nullary query true"
9376        );
9377        Ok(())
9378    }
9379
9380    #[test]
9381    fn caller_input_is_unioned_with_inline_facts_before_execution() -> Result<()> {
9382        let Some(provider) = ground_term_encoding_test_provider() else {
9383            return Ok(());
9384        };
9385        let program = LogicProgram::compile(
9386            r#"
9387                pred source(u32).
9388                pred result(u32).
9389                source(1).
9390                result(X) :- source(X).
9391                ?- result(X).
9392            "#,
9393        )?;
9394        let source_schema = program
9395            .schema("source")
9396            .ok_or_else(|| XlogError::Execution("missing source schema".to_string()))?
9397            .clone();
9398        let caller_value = 2u32.to_le_bytes();
9399        let caller_input =
9400            provider.create_buffer_from_slices(&[caller_value.as_slice()], source_schema)?;
9401
9402        let result = program.evaluate(
9403            provider.clone(),
9404            HashMap::from([("source".to_string(), caller_input)]),
9405        )?;
9406        let mut rows = provider.download_column::<u32>(&result.queries[0].buffer, 0)?;
9407        rows.sort_unstable();
9408        assert_eq!(
9409            rows,
9410            vec![1, 2],
9411            "caller-provided rows and inline facts must both reach the executable plan"
9412        );
9413        Ok(())
9414    }
9415
9416    fn recursive_duplicate_fact_profile(
9417        provider: Arc<CudaKernelProvider>,
9418        fact_count: usize,
9419    ) -> Result<(usize, usize, usize, Vec<(u32, u32)>)> {
9420        let facts = "edge(1, 2).\n".repeat(fact_count);
9421        let program = LogicProgram::compile(&format!(
9422            r#"
9423                pred edge(u32, u32).
9424                pred reach(u32, u32).
9425                {facts}
9426                reach(X, Y) :- edge(X, Y).
9427                reach(X, Z) :- reach(X, Y), edge(Y, Z).
9428                ?- reach(X, Z).
9429            "#
9430        ))?;
9431        assert_eq!(program.program.facts().count(), fact_count);
9432        assert_eq!(
9433            program.program.proper_rules().count(),
9434            2,
9435            "the recursive rule shape must remain constant across fact counts"
9436        );
9437        let LogicExecutionPlan::Ordinary(plan) = &program.plan else {
9438            panic!("recursive source must compile to an ordinary plan");
9439        };
9440        let executable_rule_count = plan.rules_by_scc.iter().map(Vec::len).sum::<usize>();
9441
9442        let result = program.evaluate_with_options(provider.clone(), HashMap::new(), true)?;
9443        let stats = result
9444            .stats
9445            .as_ref()
9446            .ok_or_else(|| XlogError::Execution("missing execution profile".to_string()))?;
9447        let scan_count = stats
9448            .strata
9449            .iter()
9450            .flat_map(|stratum| &stratum.ops)
9451            .filter(|op| op.op_name == "scan")
9452            .count();
9453        let union_count = stats
9454            .strata
9455            .iter()
9456            .flat_map(|stratum| &stratum.ops)
9457            .filter(|op| op.op_name == "union")
9458            .count();
9459        let xs = provider.download_column::<u32>(&result.queries[0].buffer, 0)?;
9460        let ys = provider.download_column::<u32>(&result.queries[0].buffer, 1)?;
9461        let mut rows = xs.into_iter().zip(ys).collect::<Vec<_>>();
9462        rows.sort_unstable();
9463        Ok((executable_rule_count, scan_count, union_count, rows))
9464    }
9465
9466    #[test]
9467    fn recursive_plan_operations_are_invariant_to_duplicate_fact_count() -> Result<()> {
9468        let Some(provider) = ground_term_encoding_test_provider() else {
9469            return Ok(());
9470        };
9471
9472        let one_fact = recursive_duplicate_fact_profile(provider.clone(), 1)?;
9473        let many_facts = recursive_duplicate_fact_profile(provider, 64)?;
9474        assert_eq!(one_fact.3, vec![(1, 2)]);
9475        assert_eq!(many_facts.3, one_fact.3);
9476        assert_eq!(
9477            many_facts.0, one_fact.0,
9478            "executable rule count must not scale with source fact count"
9479        );
9480        assert_eq!(
9481            many_facts.1, one_fact.1,
9482            "executable scan count must not scale with source fact count"
9483        );
9484        assert_eq!(
9485            many_facts.2, one_fact.2,
9486            "executable union count must not scale with source fact count"
9487        );
9488        Ok(())
9489    }
9490
9491    #[test]
9492    fn g91_compatibility_plan_records_gpu_upper_and_refinement_passes() -> Result<()> {
9493        let program = LogicProgram::compile(
9494            r#"
9495                #pragma epistemic_mode = g91
9496                pred domain(u32).
9497                pred p(u32).
9498                pred q(u32).
9499                domain(7).
9500                p(X) :- domain(X), possible q(X).
9501                q(X) :- domain(X), possible p(X).
9502                ?- p(X).
9503                ?- q(X).
9504            "#,
9505        )?;
9506
9507        let LogicExecutionPlan::EpistemicG91Compatibility(plan) = &program.plan else {
9508            panic!("mutual G91 possibility cycle must select compatibility iteration");
9509        };
9510        assert_eq!(plan.snapshot_relations.len(), 2);
9511        assert_eq!(plan.convergence_predicates, vec!["p", "q"]);
9512        let summary = program
9513            .epistemic_plan_json()
9514            .expect("epistemic compatibility summary");
9515        assert!(summary.contains("epistemic_g91_compatibility_gpu"));
9516        assert!(summary.contains("\"gpu_passes\":[\"upper_bound\",\"refinement\"]"));
9517        assert!(summary.contains("\"execution_backend\":\"gpu\""));
9518        assert!(summary.contains("\"fallback_policy\":\"reject_unsupported\""));
9519        assert!(!summary.contains("\"cpu_fallback_total_zero\""));
9520        assert!(!summary.contains("\"cpu_fallback_is_zero\""));
9521        Ok(())
9522    }
9523
9524    #[test]
9525    fn g91_compatibility_infers_undeclared_snapshot_schema() -> Result<()> {
9526        let program = LogicProgram::compile(
9527            r#"
9528                #pragma epistemic_mode = g91
9529                domain(1).
9530                p(X) :- domain(X), possible p(X).
9531                ?- p(X).
9532            "#,
9533        )?;
9534
9535        let LogicExecutionPlan::EpistemicG91Compatibility(plan) = &program.plan else {
9536            panic!("undeclared G91 relation must select compatibility iteration");
9537        };
9538        let snapshot = plan
9539            .snapshot_relations
9540            .get("p")
9541            .expect("snapshot name for p");
9542        let refinement_schemas = gpu_evaluation_pass_schemas(&plan.refinement);
9543        let schema = refinement_schemas
9544            .get(snapshot)
9545            .expect("inferred snapshot schema");
9546        assert_eq!(schema.arity(), 1);
9547        assert_eq!(schema.column_type(0), Some(ScalarType::U32));
9548        Ok(())
9549    }
9550
9551    #[test]
9552    fn g91_compatibility_compile_rejects_a_recursive_aggregate_component() {
9553        let result = LogicProgram::compile(
9554            r#"
9555                #pragma epistemic_mode = g91
9556                pred seed(u32).
9557                pred p(u32).
9558                pred totals(u64).
9559                seed(1).
9560                p(X) :- seed(X), possible p(X).
9561                p(X) :- seed(X), totals(_).
9562                totals(count(X)) :- p(X).
9563                ?- p(X).
9564            "#,
9565        );
9566        let error = match result {
9567            Ok(_) => panic!("recursive aggregation must not enter compatibility refinement"),
9568            Err(error) => error,
9569        };
9570        let message = error.to_string();
9571        assert!(message.contains("Gelfond-1991 compatibility"), "{message}");
9572        assert!(message.contains("aggregate"), "{message}");
9573        assert!(message.contains("totals"), "{message}");
9574    }
9575
9576    #[test]
9577    fn g91_introspection_preserves_authored_rules_and_queries() -> Result<()> {
9578        let program = LogicProgram::compile(
9579            r#"
9580                #pragma epistemic_mode = g91
9581                pred base(u32).
9582                pred p(u32).
9583                base(1).
9584                p(X) :- base(X), possible p(X).
9585                ?- p(X).
9586            "#,
9587        )?;
9588
9589        let provenance = program.rule_provenance();
9590        let p_rule = provenance
9591            .iter()
9592            .find(|rule| rule.head == "p(X)")
9593            .expect("authored p rule provenance");
9594        assert_eq!(p_rule.support_relation_ids, vec!["base", "p"]);
9595        assert!(provenance.iter().all(|rule| {
9596            rule.support_relation_ids
9597                .iter()
9598                .all(|relation| !relation.starts_with("__xlog_"))
9599        }));
9600
9601        let traces = program.proof_traces();
9602        assert_eq!(traces.len(), 1);
9603        assert_eq!(traces[0].query, "p(X)");
9604        assert!(traces[0].source_facts.iter().any(|fact| fact == "base(1)."));
9605        Ok(())
9606    }
9607
9608    #[test]
9609    fn predicate_function_proof_traces_preserve_source_names_and_normalized_support() -> Result<()>
9610    {
9611        let program = LogicProgram::compile(
9612            r#"
9613                pred candidate(i32, i32).
9614                pred blocked(i32).
9615                pred forbidden(i32).
9616                pred answer(i32).
9617                candidate(1, 2).
9618                func visible(X) = Y :- candidate(X, Y), not blocked(Y).
9619                answer(Y) :- Y is visible(1), not forbidden(Y).
9620                ?- answer(Y).
9621            "#,
9622        )?;
9623
9624        let provenance = program.rule_provenance();
9625        let answer_rule = provenance
9626            .iter()
9627            .find(|rule| rule.head == "answer(Y)")
9628            .expect("answer rule provenance");
9629        assert!(
9630            ["candidate", "blocked", "forbidden"]
9631                .iter()
9632                .all(|relation| answer_rule
9633                    .support_relation_ids
9634                    .iter()
9635                    .any(|id| id == relation)),
9636            "{:?}",
9637            answer_rule.support_relation_ids
9638        );
9639
9640        let traces = program.proof_traces();
9641        assert_eq!(traces.len(), 1);
9642        assert!(traces[0]
9643            .rejected_alternatives
9644            .iter()
9645            .any(|alternative| alternative == "not blocked(Y)"));
9646        assert!(traces[0]
9647            .rejected_alternatives
9648            .iter()
9649            .any(|alternative| alternative == "not forbidden(Y)"));
9650        assert!(traces[0]
9651            .source_facts
9652            .iter()
9653            .any(|fact| fact == "candidate(1, 2)."));
9654        assert!(!format!("{provenance:?}{traces:?}").contains("__XLOG_FUNCTION"));
9655        Ok(())
9656    }
9657
9658    #[test]
9659    fn relation_clone_context_preserves_cuda_error_variants() {
9660        let kernel = relation_clone_error(
9661            "cloning relation 'fact'".to_string(),
9662            XlogError::Kernel("launch failed".to_string()),
9663        );
9664        assert!(matches!(
9665            kernel,
9666            XlogError::Kernel(message)
9667                if message == "cloning relation 'fact': launch failed"
9668        ));
9669
9670        let exhausted = relation_clone_error(
9671            "cloning relation 'fact'".to_string(),
9672            XlogError::ResourceExhausted {
9673                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(),
9674                estimated_bytes: 64,
9675                budget_bytes: 63,
9676            },
9677        );
9678        match exhausted {
9679            XlogError::ResourceExhausted {
9680                context,
9681                estimated_bytes,
9682                budget_bytes,
9683            } => {
9684                assert_eq!(
9685                    context,
9686                    "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"
9687                );
9688                assert_eq!(estimated_bytes, 64);
9689                assert_eq!(budget_bytes, 63);
9690            }
9691            error => panic!("expected resource exhaustion, got {error}"),
9692        }
9693    }
9694
9695    #[test]
9696    fn compiled_argument_preserves_declared_names_domains_and_order() -> Result<()> {
9697        let program = LogicProgram::compile(
9698            r#"
9699                domain party: u32.
9700                pred transfer(giver: party, receiver: party, asset: u32, time: i64).
9701                pred positional(u32, i64, symbol).
9702            "#,
9703        )?;
9704
9705        let transfer = program
9706            .argument_schema("transfer")
9707            .expect("compiled transfer argument schema");
9708        assert_eq!(
9709            transfer
9710                .iter()
9711                .map(|argument| (
9712                    argument.name(),
9713                    argument.source_named(),
9714                    argument.sort(),
9715                    argument.scalar_type(),
9716                ))
9717                .collect::<Vec<_>>(),
9718            vec![
9719                ("giver", true, Some("party"), ScalarType::U32),
9720                ("receiver", true, Some("party"), ScalarType::U32),
9721                ("asset", true, None, ScalarType::U32),
9722                ("time", true, None, ScalarType::I64),
9723            ]
9724        );
9725
9726        let positional = program
9727            .argument_schema("positional")
9728            .expect("compiled positional argument schema");
9729        assert_eq!(
9730            positional
9731                .iter()
9732                .map(|argument| (
9733                    argument.name(),
9734                    argument.source_named(),
9735                    argument.sort(),
9736                    argument.scalar_type(),
9737                ))
9738                .collect::<Vec<_>>(),
9739            vec![
9740                ("c0", false, None, ScalarType::U32),
9741                ("c1", false, None, ScalarType::I64),
9742                ("c2", false, None, ScalarType::Symbol),
9743            ]
9744        );
9745
9746        Ok(())
9747    }
9748
9749    #[test]
9750    fn compiled_argument_uses_schema_metadata_for_inferred_relations() -> Result<()> {
9751        let program = LogicProgram::compile(
9752            r#"
9753                pred source(value: i64).
9754                inferred(X) :- source(X).
9755            "#,
9756        )?;
9757
9758        let inferred = program
9759            .argument_schema("inferred")
9760            .expect("compiled inferred argument schema");
9761        assert_eq!(
9762            inferred
9763                .iter()
9764                .map(|argument| (
9765                    argument.name(),
9766                    argument.source_named(),
9767                    argument.sort(),
9768                    argument.scalar_type(),
9769                ))
9770                .collect::<Vec<_>>(),
9771            vec![("c0", false, None, ScalarType::I64)]
9772        );
9773        assert!(program.argument_schema("unknown").is_none());
9774
9775        Ok(())
9776    }
9777
9778    #[test]
9779    fn compiled_argument_recovers_source_metadata_for_arity_qualified_relations() -> Result<()> {
9780        let program = LogicProgram::compile(
9781            r#"
9782                domain identity: u32.
9783                pred joined(u32, u32).
9784                pred seed(u32, u32).
9785                pred polymorphic(value: identity).
9786                pred polymorphic(left: identity, right: identity).
9787
9788                joined(X, Y) :- seed(X, Y), know polymorphic(X), possible polymorphic(X, Y).
9789            "#,
9790        )?;
9791
9792        let unary = program
9793            .argument_schema("polymorphic/1")
9794            .expect("compiled arity-qualified argument schema");
9795        assert_eq!(
9796            unary
9797                .iter()
9798                .map(|argument| (
9799                    argument.name(),
9800                    argument.source_named(),
9801                    argument.sort(),
9802                    argument.scalar_type(),
9803                ))
9804                .collect::<Vec<_>>(),
9805            vec![("value", true, Some("identity"), ScalarType::U32)]
9806        );
9807
9808        Ok(())
9809    }
9810
9811    #[test]
9812    fn epistemic_compile_qualifies_extensional_signatures_used_by_reduction() -> Result<()> {
9813        let program = LogicProgram::compile(
9814            r#"
9815                #pragma epistemic_mode = faeel
9816                pred node(symbol).
9817                pred source(symbol, i64).
9818                pred source(u32).
9819                pred result(symbol).
9820                node(key).
9821                source(key, 5000000000).
9822                source(1).
9823                result(X) :- node(X), know source(X, Y).
9824                ?- result(X).
9825            "#,
9826        )?;
9827
9828        let source = program
9829            .schema("source/2")
9830            .expect("missing arity-qualified binary source schema");
9831        assert_eq!(source.column_type(0), Some(ScalarType::Symbol));
9832        assert_eq!(source.column_type(1), Some(ScalarType::I64));
9833        assert!(program.schema("source/1").is_some());
9834        assert_eq!(
9835            program
9836                .schema("result")
9837                .expect("missing augmented result schema")
9838                .arity(),
9839            2
9840        );
9841
9842        Ok(())
9843    }
9844
9845    #[test]
9846    fn epistemic_compile_infers_hidden_column_types_from_runtime_binders() -> Result<()> {
9847        let inferred_source = LogicProgram::compile(
9848            r#"
9849                #pragma epistemic_mode = faeel
9850                pred node(symbol).
9851                pred result(symbol).
9852                node(key).
9853                raw(key, 5000000000).
9854                edge(X, Y) :- raw(X, Y).
9855                result(X) :- node(X), know edge(X, Y).
9856                ?- result(X).
9857            "#,
9858        )?;
9859        assert_eq!(
9860            inferred_source
9861                .schema("result")
9862                .expect("missing inferred-source result schema")
9863                .column_type(1),
9864            Some(ScalarType::I64)
9865        );
9866
9867        let arithmetic_binding = LogicProgram::compile(
9868            r#"
9869                #pragma epistemic_mode = faeel
9870                pred node(symbol).
9871                pred allowed(u64).
9872                pred result(symbol).
9873                node(key).
9874                allowed(1).
9875                result(X) :- node(X), Y is cast(1, u64), not know allowed(Y).
9876                ?- result(X).
9877            "#,
9878        )?;
9879        assert_eq!(
9880            arithmetic_binding
9881                .schema("result")
9882                .expect("missing arithmetic-bound result schema")
9883                .column_type(1),
9884            Some(ScalarType::U64)
9885        );
9886
9887        Ok(())
9888    }
9889
9890    #[test]
9891    fn epistemic_compile_uses_one_extensional_arity_census() -> Result<()> {
9892        let program = LogicProgram::compile(
9893            r#"
9894                #pragma epistemic_mode = faeel
9895                p(a).
9896                result(X) :- p(X), know p(X).
9897                :- p(X, Y).
9898                ?- result(X).
9899            "#,
9900        )?;
9901
9902        assert!(program.schema("p/1").is_some());
9903        assert!(program.schema("p/2").is_some());
9904        Ok(())
9905    }
9906
9907    #[test]
9908    fn epistemic_compile_preserves_recursive_stratum_schema() -> Result<()> {
9909        let program = LogicProgram::compile(
9910            r#"
9911                #pragma epistemic_mode = faeel
9912                pred node(u32).
9913                pred edge(u32, u32).
9914                pred accepted_edge(u32, u32).
9915                pred reach(u32, u32).
9916                node(1).
9917                node(2).
9918                node(3).
9919                edge(1, 2).
9920                edge(2, 3).
9921                accepted_edge(X, Y) :- node(X), node(Y), know edge(X, Y).
9922                reach(X, Y) :- node(X), node(Y), know accepted_edge(X, Y).
9923                reach(X, Z) :- reach(X, Y), node(Z), know accepted_edge(Y, Z).
9924                ?- reach(X, Z).
9925            "#,
9926        )?;
9927
9928        assert_eq!(
9929            program
9930                .schema("reach")
9931                .expect("missing recursive reach schema")
9932                .arity(),
9933            2
9934        );
9935        assert_eq!(program.plan_kind_label(), "epistemic_stratified");
9936        Ok(())
9937    }
9938
9939    #[test]
9940    fn epistemic_compile_rejects_unscoped_same_head_rule_unions() {
9941        let error = LogicProgram::compile(
9942            r#"
9943                #pragma epistemic_mode = faeel
9944                pred p().
9945                pred q().
9946                pred result(symbol).
9947                q().
9948                result(a) :- know p().
9949                result(b) :- know q().
9950                ?- result(X).
9951            "#,
9952        )
9953        .err()
9954        .expect("same-head modal clauses require per-clause provenance");
9955        let message = error.to_string();
9956        assert!(message.contains("epistemic rule-union materialization"));
9957        assert!(message.contains("result/1"), "{message}");
9958    }
9959
9960    #[test]
9961    fn epistemic_compile_rejects_derived_source_arity_collisions() {
9962        let sources = [
9963            r#"
9964                #pragma epistemic_mode = faeel
9965                unary(a).
9966                binary(a, b).
9967                result(X) :- unary(X), know unary(X).
9968                result(X, Y) :- binary(X, Y), know binary(X, Y).
9969            "#,
9970            r#"
9971                #pragma epistemic_mode = faeel
9972                node(key).
9973                edge(key, 5000000000).
9974                result(X) :- node(X), know edge(X, Y).
9975                ?- result(A, B).
9976            "#,
9977        ];
9978
9979        for source in sources {
9980            let error = match LogicProgram::compile(source) {
9981                Ok(_) => panic!("derived source-arity collisions must fail compilation"),
9982                Err(error) => error,
9983            };
9984            assert!(
9985                matches!(
9986                    &error,
9987                    XlogError::UnsupportedEpistemicConstruct { construct, .. }
9988                        if construct == "epistemic derived predicate schema"
9989                ),
9990                "{error}"
9991            );
9992        }
9993    }
9994
9995    #[test]
9996    fn epistemic_compile_rejects_constrained_augmented_head_query() {
9997        let error = match LogicProgram::compile(
9998            r#"
9999                #pragma epistemic_mode = faeel
10000                node(key).
10001                edge(key, 5000000000).
10002                result(X) :- node(X), know edge(X, Y).
10003                ?- result(other).
10004            "#,
10005        ) {
10006            Ok(_) => panic!("a constrained augmented-head query must fail compilation"),
10007            Err(error) => error,
10008        };
10009
10010        assert!(
10011            matches!(
10012                &error,
10013                XlogError::UnsupportedEpistemicConstruct { construct, .. }
10014                    if construct == "epistemic augmented head query"
10015            ),
10016            "{error}"
10017        );
10018    }
10019
10020    #[test]
10021    fn epistemic_compile_rejects_divergent_ordinary_bound_internal_arities() {
10022        let error = match LogicProgram::compile(
10023            r#"
10024                #pragma epistemic_mode = faeel
10025                pred node(symbol).
10026                pred edge(symbol, i64).
10027                pred allowed(i64).
10028                pred result(symbol).
10029                node(key).
10030                edge(key, 5000000000).
10031                allowed(5000000000).
10032                result(X) :- node(X).
10033                result(X) :- node(X), edge(X, Y), know allowed(Y).
10034                ?- result(X).
10035            "#,
10036        ) {
10037            Ok(_) => panic!("divergent internal arities must fail compilation"),
10038            Err(error) => error,
10039        };
10040
10041        assert!(
10042            matches!(
10043                &error,
10044                XlogError::UnsupportedEpistemicConstruct { construct, .. }
10045                    if construct == "epistemic augmented predicate schema"
10046            ),
10047            "{error}"
10048        );
10049    }
10050
10051    #[test]
10052    fn compiled_argument_preserves_declared_prefix_when_schema_is_wider() -> Result<()> {
10053        let program = LogicProgram::compile(
10054            r#"
10055                domain identity: u32.
10056                pred node(id: identity).
10057                pred edge(source: identity, target: identity).
10058                pred one_hop(node: identity).
10059
10060                one_hop(X) :- node(X), know edge(X, Y).
10061            "#,
10062        )?;
10063
10064        let one_hop = program
10065            .argument_schema("one_hop")
10066            .expect("compiled widened argument schema");
10067        assert_eq!(
10068            one_hop
10069                .iter()
10070                .map(|argument| (
10071                    argument.name(),
10072                    argument.source_named(),
10073                    argument.sort(),
10074                    argument.scalar_type(),
10075                ))
10076                .collect::<Vec<_>>(),
10077            vec![
10078                ("node", true, Some("identity"), ScalarType::U32),
10079                ("c1", false, None, ScalarType::U32),
10080            ]
10081        );
10082
10083        Ok(())
10084    }
10085
10086    #[test]
10087    fn compiled_argument_uses_the_declaration_selected_by_compilation() -> Result<()> {
10088        let program = LogicProgram::compile(
10089            r#"
10090                domain first: u32.
10091                domain second: i64.
10092                pred duplicate(value: first).
10093                pred duplicate(value: second).
10094            "#,
10095        )?;
10096
10097        let duplicate = program
10098            .argument_schema("duplicate")
10099            .expect("compiled duplicate argument schema");
10100        assert_eq!(
10101            duplicate
10102                .iter()
10103                .map(|argument| (
10104                    argument.name(),
10105                    argument.source_named(),
10106                    argument.sort(),
10107                    argument.scalar_type(),
10108                ))
10109                .collect::<Vec<_>>(),
10110            vec![("value", true, Some("second"), ScalarType::I64)]
10111        );
10112
10113        Ok(())
10114    }
10115}
10116
10117#[cfg(test)]
10118mod relation_delta_coalesce_tests {
10119    use super::*;
10120    use std::collections::HashMap;
10121    use std::sync::Arc;
10122
10123    use xlog_core::{MemoryBudget, ScalarType};
10124    use xlog_cuda::{CudaDevice, GpuMemoryManager};
10125
10126    fn test_provider() -> Option<Arc<CudaKernelProvider>> {
10127        let provider = (|| -> Result<Arc<CudaKernelProvider>> {
10128            let device = Arc::new(CudaDevice::new(0)?);
10129            let budget = MemoryBudget::with_limit(1024 * 1024 * 1024);
10130            let memory = Arc::new(GpuMemoryManager::new(device.clone(), budget));
10131            Ok(Arc::new(CudaKernelProvider::new(device, memory)?))
10132        })();
10133
10134        finish_test_provider_setup(
10135            provider,
10136            std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1"),
10137        )
10138    }
10139
10140    #[test]
10141    #[should_panic(expected = "XLOG_REQUIRE_CUDA=1 but CUDA provider construction failed")]
10142    fn required_cuda_provider_failure_is_not_silently_skipped() {
10143        finish_test_provider_setup::<()>(
10144            Err(XlogError::Execution("forced provider failure".to_string())),
10145            true,
10146        );
10147    }
10148
10149    fn test_buffer(provider: &CudaKernelProvider, rows: &[u32]) -> CudaBuffer {
10150        let schema = Schema::new(vec![("id".to_string(), ScalarType::U32)]);
10151        let bytes: Vec<u8> = rows.iter().flat_map(|v| v.to_le_bytes()).collect();
10152        let mut col = provider.memory().alloc::<u8>(bytes.len()).expect("alloc");
10153        provider
10154            .device()
10155            .inner()
10156            .htod_sync_copy_into(&bytes, &mut col)
10157            .expect("upload rows");
10158        let mut d_num_rows = provider.memory().alloc::<u32>(1).expect("alloc rows");
10159        let row_count = rows.len() as u32;
10160        provider
10161            .device()
10162            .inner()
10163            .htod_sync_copy_into(&[row_count], &mut d_num_rows)
10164            .expect("upload row count");
10165        CudaBuffer::from_columns(vec![col.into()], rows.len() as u64, d_num_rows, schema)
10166    }
10167
10168    fn read_u32(provider: &CudaKernelProvider, buffer: &CudaBuffer) -> Vec<u32> {
10169        provider
10170            .download_column::<u32>(buffer, 0)
10171            .expect("download")
10172    }
10173
10174    fn sorted_query_rows(provider: &CudaKernelProvider, result: &LogicEvalResult) -> Vec<u32> {
10175        let mut rows = read_u32(provider, &result.queries[0].buffer);
10176        rows.sort_unstable();
10177        rows
10178    }
10179
10180    fn assert_empty_modal_cycle_query_result(
10181        provider: &CudaKernelProvider,
10182        result: &LogicEvalResult,
10183    ) -> Result<()> {
10184        assert_eq!(result.queries.len(), 1);
10185        let query = &result.queries[0];
10186        assert_eq!(query.relation_name, "p");
10187        assert!(query.columns.is_empty());
10188        assert_eq!(query.buffer.schema().arity(), 0);
10189        assert_eq!(provider.device_row_count(&query.buffer)?, 0);
10190        Ok(())
10191    }
10192
10193    #[test]
10194    fn modal_cycle_query_presentation_is_consistent_across_evaluation_apis() -> Result<()> {
10195        let Some(provider) = test_provider() else {
10196            return Ok(());
10197        };
10198        let program = LogicProgram::compile(
10199            r#"
10200                #pragma epistemic_mode = faeel
10201                pred p().
10202                p() :- possible p().
10203                ?- p().
10204            "#,
10205        )?;
10206
10207        let direct = program.evaluate(provider.clone(), HashMap::new())?;
10208        assert_empty_modal_cycle_query_result(provider.as_ref(), &direct)?;
10209
10210        let relation_store = program.create_relation_store(provider.clone())?;
10211        let (from_store, cached_store) = program.evaluate_with_relation_store_and_cache(
10212            provider.clone(),
10213            &relation_store,
10214            false,
10215        )?;
10216        assert_empty_modal_cycle_query_result(provider.as_ref(), &from_store)?;
10217
10218        let cached = program.evaluate_cached_relation_store(provider.clone(), &cached_store)?;
10219        assert_empty_modal_cycle_query_result(provider.as_ref(), &cached)?;
10220
10221        let mut runtime =
10222            program.create_session_runtime(provider.clone(), &relation_store, false)?;
10223        let (from_session, _) =
10224            program.evaluate_with_session_runtime(provider.clone(), &mut runtime)?;
10225        assert_empty_modal_cycle_query_result(provider.as_ref(), &from_session)?;
10226        Ok(())
10227    }
10228
10229    #[test]
10230    fn coalesce_batch_cancels_insert_delete_pairs_on_device() {
10231        let provider = match test_provider() {
10232            Some(provider) => provider,
10233            None => {
10234                eprintln!("Skipping test: no CUDA device available");
10235                return;
10236            }
10237        };
10238
10239        let batch = vec![
10240            (
10241                "streamed_fact".to_string(),
10242                RelationDelta::new(Some(test_buffer(&provider, &[7, 8])), None),
10243            ),
10244            (
10245                "streamed_fact".to_string(),
10246                RelationDelta::new(None, Some(test_buffer(&provider, &[8]))),
10247            ),
10248            (
10249                "streamed_fact".to_string(),
10250                RelationDelta::new(Some(test_buffer(&provider, &[9])), None),
10251            ),
10252        ];
10253
10254        let report = coalesce_relation_delta_batch_with_cancellation_capture(
10255            provider.as_ref(),
10256            batch,
10257            &BTreeSet::new(),
10258        )
10259        .expect("coalesce relation delta batch");
10260        let delta = report
10261            .deltas
10262            .get("streamed_fact")
10263            .expect("coalesced relation");
10264        let insert = delta.insert.as_ref().expect("coalesced insert");
10265        assert_eq!(read_u32(&provider, insert), vec![7, 9]);
10266        assert!(delta.delete.as_ref().map(|b| b.is_empty()).unwrap_or(true));
10267        assert_eq!(report.report_seed.input_delta_count, 3);
10268        assert_eq!(report.report_seed.changed_relations, 1);
10269        assert_eq!(report.report_seed.coalesced_insert_rows, 2);
10270        assert_eq!(report.report_seed.coalesced_delete_rows, 0);
10271        assert_eq!(report.report_seed.canceled_rows, 1);
10272    }
10273
10274    #[test]
10275    fn relation_delta_batch_updates_runtime_store_and_reports_coalesced_counts() -> Result<()> {
10276        let Some(provider) = test_provider() else {
10277            eprintln!("Skipping test: no CUDA device available");
10278            return Ok(());
10279        };
10280
10281        let source = r#"
10282            pred streamed_fact(u32).
10283            pred out(u32).
10284
10285            out(X) :- streamed_fact(X).
10286
10287            ?- out(X).
10288        "#;
10289        let program = LogicProgram::compile(source)?;
10290        let mut coalesced_store = program.create_relation_store(provider.clone())?;
10291        let mut coalesced_cache = None;
10292
10293        provider.reset_host_transfer_stats();
10294        provider.reset_d2h_transfer_count();
10295        let report = program.apply_relation_delta_batch(
10296            provider.clone(),
10297            &mut coalesced_store,
10298            &mut coalesced_cache,
10299            vec![
10300                (
10301                    "streamed_fact".to_string(),
10302                    RelationDelta::new(Some(test_buffer(&provider, &[1, 2, 3])), None),
10303                ),
10304                (
10305                    "streamed_fact".to_string(),
10306                    RelationDelta::new(None, Some(test_buffer(&provider, &[2]))),
10307                ),
10308                (
10309                    "streamed_fact".to_string(),
10310                    RelationDelta::new(Some(test_buffer(&provider, &[4])), None),
10311                ),
10312            ],
10313        )?;
10314        let transfer_stats = provider.host_transfer_stats();
10315
10316        assert_eq!(report.input_delta_count, 3);
10317        assert_eq!(report.changed_relations, 1);
10318        assert_eq!(report.insert_rows, 3);
10319        assert_eq!(report.delete_rows, 0);
10320        assert_eq!(report.coalesced_insert_rows, 3);
10321        assert_eq!(report.coalesced_delete_rows, 0);
10322        assert_eq!(report.canceled_rows, 1);
10323        assert_eq!(transfer_stats.dtoh_bytes, 0);
10324        assert_eq!(transfer_stats.dtoh_calls, 0);
10325        assert_eq!(provider.d2h_transfer_count(), 0);
10326
10327        let coalesced = program.evaluate_cached_relation_store(
10328            provider.clone(),
10329            coalesced_cache
10330                .as_ref()
10331                .expect("cached store after delta batch"),
10332        )?;
10333        let coalesced_rows = sorted_query_rows(&provider, &coalesced);
10334
10335        let mut sequential_store = program.create_relation_store(provider.clone())?;
10336        let mut sequential_cache = None;
10337        for delta in [
10338            RelationDelta::new(Some(test_buffer(&provider, &[1, 2, 3])), None),
10339            RelationDelta::new(None, Some(test_buffer(&provider, &[2]))),
10340            RelationDelta::new(Some(test_buffer(&provider, &[4])), None),
10341        ] {
10342            program.apply_relation_deltas(
10343                provider.clone(),
10344                &mut sequential_store,
10345                &mut sequential_cache,
10346                HashMap::from([("streamed_fact".to_string(), delta)]),
10347            )?;
10348        }
10349        let sequential = program.evaluate_cached_relation_store(
10350            provider.clone(),
10351            sequential_cache
10352                .as_ref()
10353                .expect("cached store after sequential deltas"),
10354        )?;
10355        let sequential_rows = sorted_query_rows(&provider, &sequential);
10356
10357        let mut replacement_store = program.create_relation_store(provider.clone())?;
10358        replacement_store.put("streamed_fact", test_buffer(&provider, &[1, 3, 4]));
10359        let replacement =
10360            program.evaluate_with_relation_store(provider.clone(), &replacement_store, false)?;
10361        let replacement_rows = sorted_query_rows(&provider, &replacement);
10362
10363        assert_eq!(coalesced_rows, vec![1, 3, 4]);
10364        assert_eq!(coalesced_rows, sequential_rows);
10365        assert_eq!(coalesced_rows, replacement_rows);
10366        Ok(())
10367    }
10368}
10369
10370#[cfg(test)]
10371mod relation_delta_preparation_tests {
10372    use super::*;
10373    use std::sync::Arc;
10374
10375    use xlog_core::{MemoryBudget, ScalarType};
10376    use xlog_cuda::{CudaDevice, GpuMemoryManager};
10377
10378    fn test_provider_with_budget(limit: u64) -> Option<Arc<CudaKernelProvider>> {
10379        let provider = (|| -> Result<Arc<CudaKernelProvider>> {
10380            let device = Arc::new(CudaDevice::new(0)?);
10381            let budget = MemoryBudget::with_limit(limit);
10382            let memory = Arc::new(GpuMemoryManager::new(device.clone(), budget));
10383            Ok(Arc::new(CudaKernelProvider::new(device, memory)?))
10384        })();
10385
10386        match provider {
10387            Ok(provider) => Some(provider),
10388            Err(error) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
10389                panic!(
10390                    "XLOG_REQUIRE_CUDA=1 but CUDA provider construction failed: {}",
10391                    error
10392                )
10393            }
10394            Err(error) => {
10395                eprintln!("Skipping test: no CUDA device available ({error})");
10396                None
10397            }
10398        }
10399    }
10400
10401    fn test_provider() -> Option<Arc<CudaKernelProvider>> {
10402        test_provider_with_budget(1024 * 1024 * 1024)
10403    }
10404
10405    fn test_buffer(provider: &CudaKernelProvider, rows: &[u32]) -> CudaBuffer {
10406        let schema = Schema::new(vec![("id".to_string(), ScalarType::U32)]);
10407        let bytes: Vec<u8> = rows.iter().flat_map(|value| value.to_le_bytes()).collect();
10408        let mut column = provider.memory().alloc::<u8>(bytes.len()).expect("alloc");
10409        provider
10410            .device()
10411            .inner()
10412            .htod_sync_copy_into(&bytes, &mut column)
10413            .expect("upload rows");
10414        let mut device_row_count = provider.memory().alloc::<u32>(1).expect("alloc rows");
10415        provider
10416            .device()
10417            .inner()
10418            .htod_sync_copy_into(&[rows.len() as u32], &mut device_row_count)
10419            .expect("upload row count");
10420        CudaBuffer::from_columns(
10421            vec![column.into()],
10422            rows.len() as u64,
10423            device_row_count,
10424            schema,
10425        )
10426    }
10427
10428    fn sorted_u32(provider: &CudaKernelProvider, buffer: &CudaBuffer) -> Vec<u32> {
10429        let mut rows = provider
10430            .download_column::<u32>(buffer, 0)
10431            .expect("download rows");
10432        rows.sort_unstable();
10433        rows
10434    }
10435
10436    fn insert_rows(
10437        provider: &CudaKernelProvider,
10438        prepared: &PreparedRelationDeltaBatch,
10439        relation: &str,
10440    ) -> Vec<u32> {
10441        let delta = prepared
10442            .net_deltas()
10443            .get(relation)
10444            .expect("prepared relation delta");
10445        sorted_u32(
10446            provider,
10447            delta.insert.as_ref().expect("prepared net insert buffer"),
10448        )
10449    }
10450
10451    fn cancellation_batch(provider: &CudaKernelProvider) -> Vec<(String, RelationDelta)> {
10452        vec![
10453            (
10454                "fact".to_string(),
10455                RelationDelta::new(Some(test_buffer(provider, &[5])), None),
10456            ),
10457            (
10458                "fact".to_string(),
10459                RelationDelta::new(None, Some(test_buffer(provider, &[5]))),
10460            ),
10461            (
10462                "fact".to_string(),
10463                RelationDelta::new(Some(test_buffer(provider, &[6])), None),
10464            ),
10465        ]
10466    }
10467
10468    fn report_counts(report: &LogicDeltaReport) -> (usize, usize, u64, u64, u64) {
10469        (
10470            report.input_delta_count,
10471            report.changed_relations,
10472            report.coalesced_insert_rows,
10473            report.coalesced_delete_rows,
10474            report.canceled_rows,
10475        )
10476    }
10477
10478    #[test]
10479    fn prepared_batch_exposes_net_deltas_and_ordered_cancellation_buffers() -> Result<()> {
10480        let Some(provider) = test_provider() else {
10481            return Ok(());
10482        };
10483        let program = LogicProgram::compile("pred fact(u32).")?;
10484
10485        let prepared = program.prepare_relation_delta_batch(
10486            provider.as_ref(),
10487            vec![
10488                (
10489                    "fact".to_string(),
10490                    RelationDelta::new(Some(test_buffer(&provider, &[1, 2])), None),
10491                ),
10492                (
10493                    "fact".to_string(),
10494                    RelationDelta::new(None, Some(test_buffer(&provider, &[2]))),
10495                ),
10496                (
10497                    "fact".to_string(),
10498                    RelationDelta::new(None, Some(test_buffer(&provider, &[3]))),
10499                ),
10500                (
10501                    "fact".to_string(),
10502                    RelationDelta::new(Some(test_buffer(&provider, &[3, 4])), None),
10503                ),
10504            ],
10505            &BTreeSet::from(["fact".to_string()]),
10506        )?;
10507
10508        assert_eq!(insert_rows(&provider, &prepared, "fact"), vec![1, 4]);
10509        let cancellations = prepared
10510            .cancellations()
10511            .get("fact")
10512            .expect("fact cancellation trace");
10513        assert_eq!(cancellations.len(), 2);
10514        assert_eq!(cancellations[0].update_index(), 1);
10515        assert_eq!(
10516            cancellations[0].incoming_direction(),
10517            RelationDeltaDirection::Delete
10518        );
10519        assert_eq!(sorted_u32(&provider, cancellations[0].tuples()), vec![2]);
10520        assert_eq!(cancellations[1].update_index(), 3);
10521        assert_eq!(
10522            cancellations[1].incoming_direction(),
10523            RelationDeltaDirection::Insert
10524        );
10525        assert_eq!(sorted_u32(&provider, cancellations[1].tuples()), vec![3]);
10526        Ok(())
10527    }
10528
10529    #[test]
10530    fn cancellation_trace_distinguishes_canceled_and_surviving_insert_occurrences() -> Result<()> {
10531        let Some(provider) = test_provider() else {
10532            return Ok(());
10533        };
10534        let program = LogicProgram::compile("pred fact(u32).")?;
10535
10536        let insert_delete_insert = program.prepare_relation_delta_batch(
10537            provider.as_ref(),
10538            vec![
10539                (
10540                    "fact".to_string(),
10541                    RelationDelta::new(Some(test_buffer(&provider, &[7])), None),
10542                ),
10543                (
10544                    "fact".to_string(),
10545                    RelationDelta::new(None, Some(test_buffer(&provider, &[7]))),
10546                ),
10547                (
10548                    "fact".to_string(),
10549                    RelationDelta::new(Some(test_buffer(&provider, &[7])), None),
10550                ),
10551            ],
10552            &BTreeSet::from(["fact".to_string()]),
10553        )?;
10554        assert_eq!(
10555            insert_rows(&provider, &insert_delete_insert, "fact"),
10556            vec![7]
10557        );
10558        let first_trace = insert_delete_insert
10559            .cancellations()
10560            .get("fact")
10561            .expect("first cancellation trace");
10562        assert_eq!(first_trace.len(), 1);
10563        assert_eq!(first_trace[0].update_index(), 1);
10564        assert_eq!(
10565            first_trace[0].incoming_direction(),
10566            RelationDeltaDirection::Delete
10567        );
10568        assert_eq!(sorted_u32(&provider, first_trace[0].tuples()), vec![7]);
10569
10570        let delete_insert_insert = program.prepare_relation_delta_batch(
10571            provider.as_ref(),
10572            vec![
10573                (
10574                    "fact".to_string(),
10575                    RelationDelta::new(None, Some(test_buffer(&provider, &[7]))),
10576                ),
10577                (
10578                    "fact".to_string(),
10579                    RelationDelta::new(Some(test_buffer(&provider, &[7])), None),
10580                ),
10581                (
10582                    "fact".to_string(),
10583                    RelationDelta::new(Some(test_buffer(&provider, &[7])), None),
10584                ),
10585            ],
10586            &BTreeSet::from(["fact".to_string()]),
10587        )?;
10588        assert_eq!(
10589            insert_rows(&provider, &delete_insert_insert, "fact"),
10590            vec![7]
10591        );
10592        let second_trace = delete_insert_insert
10593            .cancellations()
10594            .get("fact")
10595            .expect("second cancellation trace");
10596        assert_eq!(second_trace.len(), 1);
10597        assert_eq!(second_trace[0].update_index(), 1);
10598        assert_eq!(
10599            second_trace[0].incoming_direction(),
10600            RelationDeltaDirection::Insert
10601        );
10602        assert_eq!(sorted_u32(&provider, second_trace[0].tuples()), vec![7]);
10603        Ok(())
10604    }
10605
10606    #[test]
10607    fn raw_combined_delta_preserves_delete_then_insert_while_batch_cancels() -> Result<()> {
10608        let Some(provider) = test_provider() else {
10609            return Ok(());
10610        };
10611        let program = LogicProgram::compile("pred fact(u32).")?;
10612
10613        let mut raw_store = program.create_relation_store(provider.clone())?;
10614        raw_store.put("fact", test_buffer(&provider, &[7]));
10615        let mut raw_cache = None;
10616        let mut raw_runtime = None;
10617        let mut raw_delta = HashMap::new();
10618        raw_delta.insert(
10619            "fact".to_string(),
10620            RelationDelta::new(
10621                Some(test_buffer(&provider, &[7])),
10622                Some(test_buffer(&provider, &[7])),
10623            ),
10624        );
10625
10626        let raw_commit = program.prepare_relation_deltas_commit_with_session_runtime(
10627            provider.clone(),
10628            &mut raw_store,
10629            &mut raw_cache,
10630            &mut raw_runtime,
10631            raw_delta,
10632        )?;
10633        let raw_report = raw_commit.commit();
10634        assert_eq!(
10635            sorted_u32(&provider, raw_store.get("fact").unwrap()),
10636            vec![7]
10637        );
10638        assert_eq!(raw_report.insert_rows, 1);
10639        assert_eq!(raw_report.delete_rows, 1);
10640        assert_eq!(raw_report.canceled_rows, 0);
10641        assert_eq!(raw_report.changed_relations, 1);
10642
10643        let mut batch_store = program.create_relation_store(provider.clone())?;
10644        batch_store.put("fact", test_buffer(&provider, &[7]));
10645        let before_batch_version = batch_store.version("fact");
10646        let mut batch_cache = None;
10647        let mut batch_runtime = None;
10648        let batch = program.prepare_relation_delta_batch(
10649            provider.as_ref(),
10650            vec![
10651                (
10652                    "fact".to_string(),
10653                    RelationDelta::new(Some(test_buffer(&provider, &[7])), None),
10654                ),
10655                (
10656                    "fact".to_string(),
10657                    RelationDelta::new(None, Some(test_buffer(&provider, &[7]))),
10658                ),
10659            ],
10660            &BTreeSet::from(["fact".to_string()]),
10661        )?;
10662        let batch_commit = program.prepare_relation_delta_commit_with_session_runtime(
10663            provider.clone(),
10664            &mut batch_store,
10665            &mut batch_cache,
10666            &mut batch_runtime,
10667            batch,
10668        )?;
10669        let batch_report = batch_commit.commit();
10670        assert_eq!(batch_store.version("fact"), before_batch_version);
10671        assert_eq!(
10672            sorted_u32(&provider, batch_store.get("fact").unwrap()),
10673            vec![7]
10674        );
10675        assert_eq!(batch_report.insert_rows, 0);
10676        assert_eq!(batch_report.delete_rows, 0);
10677        assert_eq!(batch_report.canceled_rows, 1);
10678        assert_eq!(batch_report.changed_relations, 0);
10679        Ok(())
10680    }
10681
10682    #[test]
10683    fn cancellation_capture_is_scoped_to_selected_relations() -> Result<()> {
10684        let Some(provider) = test_provider() else {
10685            return Ok(());
10686        };
10687        let program = LogicProgram::compile(
10688            r#"
10689                pred evidenced(u32).
10690                pred positional(u32).
10691            "#,
10692        )?;
10693        let prepared = program.prepare_relation_delta_batch(
10694            provider.as_ref(),
10695            vec![
10696                (
10697                    "evidenced".to_string(),
10698                    RelationDelta::new(Some(test_buffer(&provider, &[1])), None),
10699                ),
10700                (
10701                    "positional".to_string(),
10702                    RelationDelta::new(Some(test_buffer(&provider, &[2])), None),
10703                ),
10704                (
10705                    "evidenced".to_string(),
10706                    RelationDelta::new(None, Some(test_buffer(&provider, &[1]))),
10707                ),
10708                (
10709                    "positional".to_string(),
10710                    RelationDelta::new(None, Some(test_buffer(&provider, &[2]))),
10711                ),
10712                (
10713                    "evidenced".to_string(),
10714                    RelationDelta::new(None, Some(test_buffer(&provider, &[3]))),
10715                ),
10716                (
10717                    "evidenced".to_string(),
10718                    RelationDelta::new(Some(test_buffer(&provider, &[3])), None),
10719                ),
10720            ],
10721            &BTreeSet::from(["evidenced".to_string()]),
10722        )?;
10723
10724        assert_eq!(
10725            prepared
10726                .cancellations()
10727                .keys()
10728                .map(String::as_str)
10729                .collect::<BTreeSet<_>>(),
10730            BTreeSet::from(["evidenced"])
10731        );
10732        let evidenced = prepared
10733            .cancellations()
10734            .get("evidenced")
10735            .expect("selected relation cancellation trace");
10736        assert_eq!(
10737            evidenced
10738                .iter()
10739                .map(RelationDeltaCancellation::update_index)
10740                .collect::<Vec<_>>(),
10741            vec![2, 5]
10742        );
10743        assert_eq!(prepared.report_seed.canceled_rows, 3);
10744        Ok(())
10745    }
10746
10747    #[test]
10748    fn disabled_cancellation_capture_preserves_net_data_and_stats_without_trace_work() -> Result<()>
10749    {
10750        let Some(provider) = test_provider() else {
10751            return Ok(());
10752        };
10753        let program = LogicProgram::compile(
10754            r#"
10755                pred fact(u32).
10756                pred out(u32).
10757                out(X) :- fact(X).
10758                ?- out(X).
10759            "#,
10760        )?;
10761        let mut uncaptured_store = program.create_relation_store(provider.clone())?;
10762        let mut uncaptured_cache = None;
10763        let mut uncaptured_runtime = None;
10764        let mut captured_store = program.create_relation_store(provider.clone())?;
10765        let mut captured_cache = None;
10766        let mut captured_runtime = None;
10767
10768        let uncaptured_batch = cancellation_batch(&provider);
10769        provider.memory().reset_alloc_count();
10770        let uncaptured = program.prepare_relation_delta_batch(
10771            provider.as_ref(),
10772            uncaptured_batch,
10773            &BTreeSet::new(),
10774        )?;
10775        let uncaptured_allocations = provider.memory().alloc_count();
10776        assert!(uncaptured.cancellations().is_empty());
10777        assert_eq!(insert_rows(&provider, &uncaptured, "fact"), vec![6]);
10778        let uncaptured_commit = program.prepare_relation_delta_commit_with_session_runtime(
10779            provider.clone(),
10780            &mut uncaptured_store,
10781            &mut uncaptured_cache,
10782            &mut uncaptured_runtime,
10783            uncaptured,
10784        )?;
10785        let uncaptured_report = uncaptured_commit.commit();
10786
10787        let captured_batch = cancellation_batch(&provider);
10788        provider.memory().reset_alloc_count();
10789        let captured = program.prepare_relation_delta_batch(
10790            provider.as_ref(),
10791            captured_batch,
10792            &BTreeSet::from(["fact".to_string()]),
10793        )?;
10794        let captured_allocations = provider.memory().alloc_count();
10795        assert_eq!(insert_rows(&provider, &captured, "fact"), vec![6]);
10796        assert_eq!(
10797            captured
10798                .cancellations()
10799                .get("fact")
10800                .expect("captured cancellation")
10801                .len(),
10802            1
10803        );
10804        assert!(
10805            captured_allocations > uncaptured_allocations,
10806            "capturing cancellation tuples should add device allocation requests in this fixture: captured={captured_allocations}, uncaptured={uncaptured_allocations}"
10807        );
10808        let captured_commit = program.prepare_relation_delta_commit_with_session_runtime(
10809            provider.clone(),
10810            &mut captured_store,
10811            &mut captured_cache,
10812            &mut captured_runtime,
10813            captured,
10814        )?;
10815        let captured_report = captured_commit.commit();
10816
10817        assert_eq!(
10818            report_counts(&uncaptured_report),
10819            report_counts(&captured_report)
10820        );
10821        assert_eq!(report_counts(&uncaptured_report), (3, 1, 1, 0, 1));
10822        assert_eq!(
10823            sorted_u32(
10824                &provider,
10825                uncaptured_store.get("fact").expect("uncaptured base fact")
10826            ),
10827            vec![6]
10828        );
10829        assert_eq!(
10830            sorted_u32(
10831                &provider,
10832                captured_store.get("fact").expect("captured base fact")
10833            ),
10834            vec![6]
10835        );
10836        Ok(())
10837    }
10838
10839    #[test]
10840    fn retained_preparation_failure_discards_mutated_runtime_without_base_puts() -> Result<()> {
10841        let Some(provider) = test_provider() else {
10842            return Ok(());
10843        };
10844        let program = LogicProgram::compile(
10845            r#"
10846                pred safe(u32).
10847                pred forbidden(u32).
10848                pred out(u32).
10849                out(X) :- safe(X).
10850                :- forbidden(X).
10851                ?- out(X).
10852            "#,
10853        )?;
10854        let mut base_store = program.create_relation_store(provider.clone())?;
10855        let safe_version = base_store.version("safe").expect("safe version");
10856        let forbidden_version = base_store.version("forbidden").expect("forbidden version");
10857        let mut runtime =
10858            Some(program.create_session_runtime(provider.clone(), &base_store, false)?);
10859        let (_, initial_cache) = program.evaluate_with_session_runtime(
10860            provider.clone(),
10861            runtime.as_mut().expect("initial runtime"),
10862        )?;
10863        let mut cache = Some(initial_cache);
10864        let prepared = program.prepare_relation_delta_batch(
10865            provider.as_ref(),
10866            vec![
10867                (
10868                    "safe".to_string(),
10869                    RelationDelta::new(Some(test_buffer(&provider, &[1])), None),
10870                ),
10871                (
10872                    "forbidden".to_string(),
10873                    RelationDelta::new(Some(test_buffer(&provider, &[9])), None),
10874                ),
10875            ],
10876            &BTreeSet::new(),
10877        )?;
10878
10879        let error = match program.prepare_relation_delta_commit_with_session_runtime(
10880            provider.clone(),
10881            &mut base_store,
10882            &mut cache,
10883            &mut runtime,
10884            prepared,
10885        ) {
10886            Ok(_) => panic!("constraint-violating preparation must fail"),
10887            Err(error) => error,
10888        };
10889
10890        assert!(
10891            error.to_string().contains("Constraint 0 violated"),
10892            "unexpected preparation error: {error}"
10893        );
10894        assert!(cache.is_none(), "failed preparation must discard cache");
10895        assert!(runtime.is_none(), "failed preparation must discard runtime");
10896        assert_eq!(base_store.version("safe"), Some(safe_version));
10897        assert_eq!(base_store.version("forbidden"), Some(forbidden_version));
10898        assert_eq!(base_store.get("safe").expect("safe base").num_rows(), 0);
10899        assert_eq!(
10900            base_store
10901                .get("forbidden")
10902                .expect("forbidden base")
10903                .num_rows(),
10904            0
10905        );
10906        Ok(())
10907    }
10908
10909    #[test]
10910    fn successful_preparation_binds_and_stages_all_state_until_infallible_commit() -> Result<()> {
10911        let Some(provider) = test_provider() else {
10912            return Ok(());
10913        };
10914        let program = LogicProgram::compile(
10915            r#"
10916                pred left_input(u32).
10917                pred right_input(u32).
10918                pred out(u32).
10919                out(X) :- left_input(X).
10920                out(X) :- right_input(X).
10921                ?- out(X).
10922            "#,
10923        )?;
10924        let mut base_store = program.create_relation_store(provider.clone())?;
10925        let left_version = base_store
10926            .version("left_input")
10927            .expect("left input version");
10928        let right_version = base_store
10929            .version("right_input")
10930            .expect("right input version");
10931        let mut runtime =
10932            Some(program.create_session_runtime(provider.clone(), &base_store, false)?);
10933        let (_, initial_cache) = program.evaluate_with_session_runtime(
10934            provider.clone(),
10935            runtime.as_mut().expect("initial runtime"),
10936        )?;
10937        let mut cache = Some(initial_cache);
10938        let authoritative_store_pointer = &mut base_store as *mut RelationStore;
10939        let cache_slot_pointer = &mut cache as *mut Option<LogicMaterializedStore>;
10940        let runtime_slot_pointer = &mut runtime as *mut Option<LogicSessionRuntime>;
10941        let prepared = program.prepare_relation_delta_batch(
10942            provider.as_ref(),
10943            vec![
10944                (
10945                    "left_input".to_string(),
10946                    RelationDelta::new(Some(test_buffer(&provider, &[1])), None),
10947                ),
10948                (
10949                    "right_input".to_string(),
10950                    RelationDelta::new(Some(test_buffer(&provider, &[2])), None),
10951                ),
10952            ],
10953            &BTreeSet::new(),
10954        )?;
10955
10956        let commit = program.prepare_relation_delta_commit_with_session_runtime(
10957            provider.clone(),
10958            &mut base_store,
10959            &mut cache,
10960            &mut runtime,
10961            prepared,
10962        )?;
10963
10964        assert!(
10965            std::ptr::eq(
10966                &*commit.authoritative_relation_store,
10967                authoritative_store_pointer
10968            ),
10969            "prepared commit must remain bound to its authoritative store"
10970        );
10971        assert!(
10972            std::ptr::eq(&*commit.cached_store_slot, cache_slot_pointer),
10973            "prepared commit must remain bound to its cache slot"
10974        );
10975        assert!(
10976            std::ptr::eq(&*commit.session_runtime_slot, runtime_slot_pointer),
10977            "prepared commit must remain bound to its runtime slot"
10978        );
10979        assert!(
10980            commit.cached_store_slot.is_none(),
10981            "prepared cache must be transaction-owned"
10982        );
10983        assert!(
10984            commit.session_runtime_slot.is_none(),
10985            "prepared runtime must be transaction-owned"
10986        );
10987        assert_eq!(
10988            commit.authoritative_relation_store.version("left_input"),
10989            Some(left_version)
10990        );
10991        assert_eq!(
10992            commit.authoritative_relation_store.version("right_input"),
10993            Some(right_version)
10994        );
10995        assert_eq!(
10996            commit
10997                .authoritative_relation_store
10998                .get("left_input")
10999                .expect("left input base")
11000                .num_rows(),
11001            0
11002        );
11003        assert_eq!(
11004            commit
11005                .authoritative_relation_store
11006                .get("right_input")
11007                .expect("right input base")
11008                .num_rows(),
11009            0
11010        );
11011        assert_eq!(commit.staged_base_updates.len(), 2);
11012        assert!(commit.prospective_cached_store.is_some());
11013        assert!(commit.prospective_session_runtime.is_some());
11014
11015        let prospective_store = commit.prospective_derived_store();
11016        assert_eq!(
11017            sorted_u32(
11018                &provider,
11019                prospective_store
11020                    .get("left_input")
11021                    .expect("prospective left input")
11022            ),
11023            vec![1]
11024        );
11025        assert_eq!(
11026            sorted_u32(
11027                &provider,
11028                prospective_store
11029                    .get("right_input")
11030                    .expect("prospective right input")
11031            ),
11032            vec![2]
11033        );
11034
11035        provider.memory().reset_alloc_count();
11036        let report = commit.commit();
11037        assert_eq!(
11038            provider.memory().alloc_count(),
11039            0,
11040            "commit must issue zero GPU allocation requests because preparation already staged every buffer"
11041        );
11042
11043        assert_eq!(base_store.version("left_input"), Some(left_version + 1));
11044        assert_eq!(base_store.version("right_input"), Some(right_version + 1));
11045        assert_eq!(
11046            sorted_u32(
11047                &provider,
11048                base_store.get("left_input").expect("committed left input")
11049            ),
11050            vec![1]
11051        );
11052        assert_eq!(
11053            sorted_u32(
11054                &provider,
11055                base_store
11056                    .get("right_input")
11057                    .expect("committed right input")
11058            ),
11059            vec![2]
11060        );
11061        let result = program.evaluate_cached_relation_store(
11062            provider.clone(),
11063            cache.as_ref().expect("committed cache"),
11064        )?;
11065        assert_eq!(sorted_u32(&provider, &result.queries[0].buffer), vec![1, 2]);
11066        assert!(runtime.is_some(), "commit must install the runtime");
11067        assert_eq!(report_counts(&report), (2, 2, 2, 0, 0));
11068        Ok(())
11069    }
11070
11071    #[test]
11072    fn prospective_base_snapshot_recomputes_deletion_without_stale_derived_rows() -> Result<()> {
11073        let Some(provider) = test_provider() else {
11074            return Ok(());
11075        };
11076        let program = LogicProgram::compile(
11077            r#"
11078                pred fact(u32).
11079                pred out(u32).
11080                out(X) :- fact(X).
11081                ?- out(X).
11082            "#,
11083        )?;
11084        let mut base_store = program.create_relation_store(provider.clone())?;
11085        base_store.put("fact", test_buffer(&provider, &[1, 2]));
11086        let mut runtime =
11087            Some(program.create_session_runtime(provider.clone(), &base_store, false)?);
11088        let (_, initial_cache) = program.evaluate_with_session_runtime(
11089            provider.clone(),
11090            runtime.as_mut().expect("initial runtime"),
11091        )?;
11092        let mut cache = Some(initial_cache);
11093        drop(base_store.remove("fact").expect("authoritative fact"));
11094
11095        let prepared = program.prepare_relation_delta_batch(
11096            provider.as_ref(),
11097            vec![(
11098                "fact".to_string(),
11099                RelationDelta::new(None, Some(test_buffer(&provider, &[2]))),
11100            )],
11101            &BTreeSet::new(),
11102        )?;
11103        let commit = program.prepare_relation_delta_commit_with_session_runtime(
11104            provider.clone(),
11105            &mut base_store,
11106            &mut cache,
11107            &mut runtime,
11108            prepared,
11109        )?;
11110
11111        let prospective_derived = commit.prospective_derived_store();
11112        assert_eq!(
11113            sorted_u32(
11114                &provider,
11115                prospective_derived
11116                    .get("__xlog_query_0")
11117                    .expect("prospective query")
11118            ),
11119            vec![1]
11120        );
11121
11122        provider.memory().reset_alloc_count();
11123        let prospective_base = commit.clone_prospective_base_store()?;
11124        assert_eq!(
11125            provider.memory().alloc_count(),
11126            4,
11127            "one-column authoritative and staged relations should each be cloned exactly once"
11128        );
11129        assert_eq!(
11130            sorted_u32(
11131                &provider,
11132                prospective_base
11133                    .get("fact")
11134                    .expect("staged missing base relation")
11135            ),
11136            vec![1]
11137        );
11138        assert_eq!(
11139            prospective_base
11140                .get("out")
11141                .expect("empty authoritative derived relation")
11142                .num_rows(),
11143            0
11144        );
11145        let (_, independently_recomputed) = program.evaluate_with_relation_store_and_cache(
11146            provider.clone(),
11147            &prospective_base,
11148            false,
11149        )?;
11150        assert!(program.relation_stores_query_equivalent(
11151            provider.as_ref(),
11152            independently_recomputed.as_relation_store(),
11153            prospective_derived,
11154        )?);
11155
11156        let mut stale_derived_seed = commit.clone_prospective_base_store()?;
11157        stale_derived_seed.put("out", test_buffer(&provider, &[1, 2]));
11158        let (_, stale_seed_recompute) = program.evaluate_with_relation_store_and_cache(
11159            provider.clone(),
11160            &stale_derived_seed,
11161            false,
11162        )?;
11163        assert_eq!(
11164            sorted_u32(
11165                &provider,
11166                stale_seed_recompute
11167                    .as_relation_store()
11168                    .get("__xlog_query_0")
11169                    .expect("stale-seeded query")
11170            ),
11171            vec![1, 2],
11172            "seeding full recompute with an intensional head retains the deleted row"
11173        );
11174        Ok(())
11175    }
11176
11177    #[test]
11178    fn prospective_base_snapshot_skips_superseded_authoritative_buffer_clone() -> Result<()> {
11179        let Some(provider) = test_provider() else {
11180            return Ok(());
11181        };
11182        let program = LogicProgram::compile(
11183            r#"
11184                pred fact(u32).
11185                pred out(u32).
11186                out(X) :- fact(X).
11187                ?- out(X).
11188            "#,
11189        )?;
11190        let mut base_store = program.create_relation_store(provider.clone())?;
11191        base_store.put("fact", test_buffer(&provider, &[1, 2]));
11192        let mut runtime =
11193            Some(program.create_session_runtime(provider.clone(), &base_store, false)?);
11194        let (_, initial_cache) = program.evaluate_with_session_runtime(
11195            provider.clone(),
11196            runtime.as_mut().expect("initial runtime"),
11197        )?;
11198        let mut cache = Some(initial_cache);
11199        let prepared = program.prepare_relation_delta_batch(
11200            provider.as_ref(),
11201            vec![(
11202                "fact".to_string(),
11203                RelationDelta::new(None, Some(test_buffer(&provider, &[2]))),
11204            )],
11205            &BTreeSet::new(),
11206        )?;
11207        let commit = program.prepare_relation_delta_commit_with_session_runtime(
11208            provider.clone(),
11209            &mut base_store,
11210            &mut cache,
11211            &mut runtime,
11212            prepared,
11213        )?;
11214
11215        provider.memory().reset_alloc_count();
11216        let prospective_base = commit.clone_prospective_base_store()?;
11217        assert_eq!(
11218            provider.memory().alloc_count(),
11219            4,
11220            "the empty authoritative head and final staged base must each be cloned once"
11221        );
11222        assert_eq!(
11223            sorted_u32(
11224                &provider,
11225                prospective_base.get("fact").expect("prospective fact")
11226            ),
11227            vec![1]
11228        );
11229        Ok(())
11230    }
11231
11232    #[test]
11233    fn prospective_base_clone_budget_failure_discards_prepared_transaction() -> Result<()> {
11234        let Some(calibration_provider) = test_provider() else {
11235            return Ok(());
11236        };
11237        let program = LogicProgram::compile(
11238            r#"
11239                pred alpha_input(u32).
11240                pred stable_input(u32).
11241            "#,
11242        )?;
11243        let stable_rows = (10_000..75_536).collect::<Vec<u32>>();
11244
11245        let mut calibration_store = program.create_relation_store(calibration_provider.clone())?;
11246        calibration_store.put(
11247            "stable_input",
11248            test_buffer(&calibration_provider, &stable_rows),
11249        );
11250        let mut calibration_runtime = Some(program.create_session_runtime(
11251            calibration_provider.clone(),
11252            &calibration_store,
11253            false,
11254        )?);
11255        let (_, calibration_cache) = program.evaluate_with_session_runtime(
11256            calibration_provider.clone(),
11257            calibration_runtime.as_mut().expect("calibration runtime"),
11258        )?;
11259        let mut calibration_cache = Some(calibration_cache);
11260        let calibration_batch = program.prepare_relation_delta_batch(
11261            calibration_provider.as_ref(),
11262            vec![(
11263                "alpha_input".to_string(),
11264                RelationDelta::new(Some(test_buffer(&calibration_provider, &[1])), None),
11265            )],
11266            &BTreeSet::new(),
11267        )?;
11268        calibration_provider.memory().reset_peak();
11269        let calibration_commit = program.prepare_relation_delta_commit_with_session_runtime(
11270            calibration_provider.clone(),
11271            &mut calibration_store,
11272            &mut calibration_cache,
11273            &mut calibration_runtime,
11274            calibration_batch,
11275        )?;
11276        assert_eq!(calibration_commit.staged_base_updates.len(), 1);
11277        assert_eq!(calibration_commit.staged_base_updates[0].0, "alpha_input");
11278        assert_eq!(calibration_commit.staged_base_updates[0].1.num_rows(), 1);
11279        let preparation_peak = calibration_provider.memory().peak_bytes();
11280        drop(calibration_commit);
11281        drop(calibration_store);
11282        drop(calibration_cache);
11283        drop(calibration_runtime);
11284        drop(calibration_provider);
11285
11286        let tight_budget = preparation_peak
11287            .checked_add(4096)
11288            .expect("calibrated preparation budget must fit in u64");
11289        let tight_provider = test_provider_with_budget(tight_budget)
11290            .expect("calibrated byte budget must construct a CUDA provider");
11291        let mut base_store = program.create_relation_store(tight_provider.clone())?;
11292        base_store.put("stable_input", test_buffer(&tight_provider, &stable_rows));
11293        let authoritative_gpu_bytes = tight_provider.memory().allocated_bytes();
11294        let alpha_version = base_store.version("alpha_input").expect("alpha version");
11295        let stable_version = base_store.version("stable_input").expect("stable version");
11296        let mut runtime =
11297            Some(program.create_session_runtime(tight_provider.clone(), &base_store, false)?);
11298        let (_, initial_cache) = program.evaluate_with_session_runtime(
11299            tight_provider.clone(),
11300            runtime.as_mut().expect("initial runtime"),
11301        )?;
11302        let mut cache = Some(initial_cache);
11303        let prepared_batch = program.prepare_relation_delta_batch(
11304            tight_provider.as_ref(),
11305            vec![(
11306                "alpha_input".to_string(),
11307                RelationDelta::new(Some(test_buffer(&tight_provider, &[1])), None),
11308            )],
11309            &BTreeSet::new(),
11310        )?;
11311        tight_provider.memory().reset_peak();
11312        let prepared_commit = program.prepare_relation_delta_commit_with_session_runtime(
11313            tight_provider.clone(),
11314            &mut base_store,
11315            &mut cache,
11316            &mut runtime,
11317            prepared_batch,
11318        )?;
11319        assert_eq!(prepared_commit.staged_base_updates.len(), 1);
11320        assert_eq!(prepared_commit.staged_base_updates[0].0, "alpha_input");
11321        assert_eq!(prepared_commit.staged_base_updates[0].1.num_rows(), 1);
11322        assert!(
11323            tight_provider.memory().peak_bytes() <= preparation_peak,
11324            "identical preparation must fit within the calibrated peak"
11325        );
11326
11327        let stable_clone_bytes = u64::try_from(stable_rows.len())
11328            .expect("stable row count must fit in u64")
11329            .checked_mul(u64::try_from(std::mem::size_of::<u32>()).expect("u32 width fits in u64"))
11330            .and_then(|bytes| {
11331                bytes.checked_add(
11332                    u64::try_from(std::mem::size_of::<u32>())
11333                        .expect("device row-count width fits in u64"),
11334                )
11335            })
11336            .expect("stable clone size must fit in u64");
11337        let staged_column_bytes =
11338            u64::try_from(std::mem::size_of::<u32>()).expect("staged column width fits in u64");
11339        let clone_headroom = stable_clone_bytes
11340            .checked_add(staged_column_bytes - 1)
11341            .expect("clone headroom must fit in u64");
11342        let pressure_bytes = tight_provider
11343            .memory()
11344            .remaining_bytes()
11345            .checked_sub(clone_headroom)
11346            .expect("calibrated provider must have room for the authoritative clone");
11347        let pressure_len = usize::try_from(pressure_bytes)
11348            .expect("calibrated pressure allocation must fit in usize");
11349        let pressure_guard = tight_provider.memory().alloc::<u8>(pressure_len)?;
11350        let clone_start_bytes = tight_provider.memory().allocated_bytes();
11351        let expected_current_bytes = clone_start_bytes
11352            .checked_add(stable_clone_bytes)
11353            .expect("current bytes before the refused staged clone must fit in u64");
11354        let expected_required_bytes = expected_current_bytes
11355            .checked_add(staged_column_bytes)
11356            .expect("cumulative required bytes must fit in u64");
11357        assert_eq!(
11358            expected_required_bytes,
11359            tight_budget + 1,
11360            "the calibrated request must exceed the configured budget by exactly one byte"
11361        );
11362
11363        let error = match prepared_commit.clone_prospective_base_store() {
11364            Ok(_) => panic!("one-byte-tight prospective base cloning must fail"),
11365            Err(error) => error,
11366        };
11367        let XlogError::ResourceExhausted {
11368            context,
11369            estimated_bytes,
11370            budget_bytes,
11371        } = &error
11372        else {
11373            panic!("expected GPU resource exhaustion, got {error}");
11374        };
11375        assert_eq!(*budget_bytes, tight_budget);
11376        assert_eq!(*estimated_bytes, expected_required_bytes);
11377        assert_eq!(
11378            context,
11379            &format!(
11380                "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}"
11381            ),
11382            "the authoritative base clone must complete before the staged overlay exhausts memory"
11383        );
11384        assert_eq!(
11385            tight_provider.memory().peak_bytes(),
11386            expected_current_bytes,
11387            "the refused request must not enter the admitted allocation high-water mark"
11388        );
11389        drop(prepared_commit);
11390
11391        assert!(cache.is_none(), "failed diagnostic must discard its cache");
11392        assert!(
11393            runtime.is_none(),
11394            "failed diagnostic must discard its retained runtime"
11395        );
11396        assert_eq!(
11397            tight_provider.memory().allocated_bytes(),
11398            authoritative_gpu_bytes + pressure_bytes,
11399            "dropping the prepared transaction must release every prospective buffer while preserving external memory pressure"
11400        );
11401        drop(pressure_guard);
11402        assert_eq!(
11403            tight_provider.memory().allocated_bytes(),
11404            authoritative_gpu_bytes,
11405            "releasing the pressure allocation must leave only authoritative data"
11406        );
11407        assert_eq!(base_store.version("alpha_input"), Some(alpha_version));
11408        assert_eq!(base_store.version("stable_input"), Some(stable_version));
11409        assert_eq!(
11410            base_store
11411                .get("alpha_input")
11412                .expect("authoritative alpha")
11413                .num_rows(),
11414            0
11415        );
11416        assert_eq!(
11417            sorted_u32(
11418                &tight_provider,
11419                base_store
11420                    .get("stable_input")
11421                    .expect("authoritative stable input")
11422            ),
11423            stable_rows
11424        );
11425        Ok(())
11426    }
11427
11428    #[test]
11429    fn later_snapshot_clone_failure_discards_staged_updates_and_derived_state() -> Result<()> {
11430        let Some(calibration_provider) = test_provider() else {
11431            return Ok(());
11432        };
11433        let program = LogicProgram::compile(
11434            r#"
11435                pred alpha_input(u32).
11436                pred omega_input(u32).
11437                pred stable_input(u32).
11438                pred out(u32).
11439                out(X) :- alpha_input(X).
11440                out(X) :- omega_input(X).
11441                ?- out(X).
11442            "#,
11443        )?;
11444        let alpha_rows = (0..512).collect::<Vec<u32>>();
11445        let omega_rows = (10_000..10_512).collect::<Vec<u32>>();
11446        let stable_rows = (20_000..85_536).collect::<Vec<u32>>();
11447
11448        let mut calibration_store = program.create_relation_store(calibration_provider.clone())?;
11449        calibration_store.put(
11450            "stable_input",
11451            test_buffer(&calibration_provider, &stable_rows),
11452        );
11453        let mut calibration_runtime = Some(program.create_session_runtime(
11454            calibration_provider.clone(),
11455            &calibration_store,
11456            false,
11457        )?);
11458        let (_, calibration_cache) = program.evaluate_with_session_runtime(
11459            calibration_provider.clone(),
11460            calibration_runtime.as_mut().expect("calibration runtime"),
11461        )?;
11462        let mut calibration_cache = Some(calibration_cache);
11463        let calibration_batch = program.prepare_relation_delta_batch(
11464            calibration_provider.as_ref(),
11465            vec![
11466                (
11467                    "alpha_input".to_string(),
11468                    RelationDelta::new(Some(test_buffer(&calibration_provider, &alpha_rows)), None),
11469                ),
11470                (
11471                    "omega_input".to_string(),
11472                    RelationDelta::new(Some(test_buffer(&calibration_provider, &omega_rows)), None),
11473                ),
11474            ],
11475            &BTreeSet::new(),
11476        )?;
11477        calibration_provider.memory().reset_peak();
11478        calibration_provider.memory().reset_alloc_count();
11479        let calibration_commit = program.prepare_relation_delta_commit_with_session_runtime(
11480            calibration_provider.clone(),
11481            &mut calibration_store,
11482            &mut calibration_cache,
11483            &mut calibration_runtime,
11484            calibration_batch,
11485        )?;
11486        let successful_peak = calibration_provider.memory().peak_bytes();
11487        let successful_preparation_allocations = calibration_provider.memory().alloc_count();
11488        assert!(
11489            successful_peak >= calibration_provider.memory().allocated_bytes(),
11490            "the calibrated peak must cover every live staged GPU allocation"
11491        );
11492        drop(calibration_commit);
11493        drop(calibration_store);
11494        drop(calibration_cache);
11495        drop(calibration_runtime);
11496        drop(calibration_provider);
11497
11498        let tight_budget = successful_peak
11499            .checked_sub(1)
11500            .expect("successful preparation must allocate GPU memory");
11501        let tight_provider = test_provider_with_budget(tight_budget)
11502            .expect("calibrated budget must still construct a CUDA provider");
11503        let mut base_store = program.create_relation_store(tight_provider.clone())?;
11504        base_store.put("stable_input", test_buffer(&tight_provider, &stable_rows));
11505        let authoritative_gpu_bytes = tight_provider.memory().allocated_bytes();
11506        let alpha_version = base_store.version("alpha_input").expect("alpha version");
11507        let omega_version = base_store.version("omega_input").expect("omega version");
11508        let stable_version = base_store.version("stable_input").expect("stable version");
11509        let mut runtime =
11510            Some(program.create_session_runtime(tight_provider.clone(), &base_store, false)?);
11511        let (_, initial_cache) = program.evaluate_with_session_runtime(
11512            tight_provider.clone(),
11513            runtime.as_mut().expect("initial runtime"),
11514        )?;
11515        let mut cache = Some(initial_cache);
11516        let prepared = program.prepare_relation_delta_batch(
11517            tight_provider.as_ref(),
11518            vec![
11519                (
11520                    "alpha_input".to_string(),
11521                    RelationDelta::new(Some(test_buffer(&tight_provider, &alpha_rows)), None),
11522                ),
11523                (
11524                    "omega_input".to_string(),
11525                    RelationDelta::new(Some(test_buffer(&tight_provider, &omega_rows)), None),
11526                ),
11527            ],
11528            &BTreeSet::new(),
11529        )?;
11530
11531        tight_provider.memory().reset_alloc_count();
11532        let final_row_count_clone_bytes =
11533            u64::try_from(std::mem::size_of::<u32>()).expect("device row-count width fits in u64");
11534        let expected_current_bytes = tight_budget
11535            .checked_sub(final_row_count_clone_bytes - 1)
11536            .expect("one-byte-tight budget must cover earlier snapshot clones");
11537        let expected_required_bytes = expected_current_bytes
11538            .checked_add(final_row_count_clone_bytes)
11539            .expect("cumulative required bytes must fit in u64");
11540        assert_eq!(
11541            expected_required_bytes,
11542            tight_budget + 1,
11543            "the calibrated final clone must exceed the configured budget by exactly one byte"
11544        );
11545        let error = match program.prepare_relation_delta_commit_with_session_runtime(
11546            tight_provider.clone(),
11547            &mut base_store,
11548            &mut cache,
11549            &mut runtime,
11550            prepared,
11551        ) {
11552            Ok(_) => panic!("one-byte-tight preparation must fail during the final clone"),
11553            Err(error) => error,
11554        };
11555        let XlogError::ResourceExhausted {
11556            context,
11557            estimated_bytes,
11558            budget_bytes,
11559        } = &error
11560        else {
11561            panic!("expected GPU resource exhaustion, got {error}");
11562        };
11563        assert_eq!(*budget_bytes, tight_budget);
11564        assert_eq!(*estimated_bytes, expected_required_bytes);
11565        assert_eq!(
11566            context,
11567            &format!(
11568                "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}"
11569            )
11570        );
11571        assert_eq!(
11572            tight_provider.memory().peak_bytes(),
11573            expected_current_bytes,
11574            "the refused final clone must leave the high-water mark at the last admitted allocation"
11575        );
11576        let failed_preparation_allocations = tight_provider.memory().alloc_count();
11577        assert!(
11578            successful_preparation_allocations > 4,
11579            "calibration must include both staged-base and snapshot clones"
11580        );
11581        assert_eq!(
11582            failed_preparation_allocations,
11583            successful_preparation_allocations,
11584            "the one-byte-tight run must reach the final calibrated clone allocation after every earlier staged clone succeeds"
11585        );
11586        assert!(cache.is_none(), "failed preparation must discard its cache");
11587        assert!(
11588            runtime.is_none(),
11589            "failed preparation must discard its runtime"
11590        );
11591        assert_eq!(
11592            tight_provider.memory().allocated_bytes(),
11593            authoritative_gpu_bytes,
11594            "failed preparation must release every transaction-owned GPU allocation"
11595        );
11596        assert_eq!(base_store.version("alpha_input"), Some(alpha_version));
11597        assert_eq!(base_store.version("omega_input"), Some(omega_version));
11598        assert_eq!(base_store.version("stable_input"), Some(stable_version));
11599        assert_eq!(
11600            base_store
11601                .get("alpha_input")
11602                .expect("authoritative alpha")
11603                .num_rows(),
11604            0
11605        );
11606        assert_eq!(
11607            base_store
11608                .get("omega_input")
11609                .expect("authoritative omega")
11610                .num_rows(),
11611            0
11612        );
11613        assert_eq!(
11614            sorted_u32(
11615                &tight_provider,
11616                base_store
11617                    .get("stable_input")
11618                    .expect("authoritative stable input")
11619            ),
11620            stable_rows
11621        );
11622        Ok(())
11623    }
11624}