1use 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
305pub struct LogicQueryResult {
307 pub relation_name: String,
310 pub columns: Vec<String>,
312 pub sort_labels: Vec<String>,
314 pub buffer: CudaBuffer,
316}
317
318pub struct LogicEvalResult {
320 pub queries: Vec<LogicQueryResult>,
322 pub stats: Option<ExecutionStats>,
324}
325
326pub 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
397pub struct LogicMaterializedStore {
403 reusable_state_identity: Arc<LogicProgramIdentity>,
404 store: RelationStore,
405}
406
407impl LogicMaterializedStore {
408 pub fn as_relation_store(&self) -> &RelationStore {
410 &self.store
411 }
412}
413
414impl LogicSessionRuntime {
415 pub fn join_index_cache_stats(&self) -> JoinIndexCacheStats {
417 self.executor.join_index_cache_stats()
418 }
419
420 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
434pub struct WcojDispatchStats {
435 pub free_join_dispatch_count: u64,
437 pub factorized_delta_dispatch_count: u64,
440 pub wcoj_groupby_fusion_dispatch_count: u64,
442 pub wcoj_error_decline_count: u64,
444}
445
446#[derive(Clone, Debug, Default, PartialEq)]
448pub struct DeltaPlannerTelemetry {
449 pub cache_reused: bool,
451 pub fallback_decision: String,
453 pub affected_sccs: usize,
455 pub recomputed_sccs: usize,
457 pub incremental_sccs: usize,
459 pub estimated_delta_speedup: Option<f64>,
461 pub measured_delta_speedup: Option<f64>,
463 pub planner_advice: Vec<String>,
465}
466
467impl DeltaPlannerTelemetry {
468 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
529pub struct LogicDeltaReport {
531 pub input_delta_count: usize,
533 pub changed_relations: usize,
535 pub changed_relation_names: Vec<String>,
537 pub insert_rows: u64,
539 pub delete_rows: u64,
541 pub has_deletes: bool,
543 pub affected_sccs: usize,
545 pub recomputed_sccs: usize,
547 pub incremental_sccs: usize,
549 pub coalesced_insert_rows: u64,
551 pub coalesced_delete_rows: u64,
553 pub canceled_rows: u64,
555 pub planner_telemetry: DeltaPlannerTelemetry,
557 pub debug_trace: Vec<String>,
559}
560
561#[derive(Clone, Copy, Debug, PartialEq, Eq)]
563pub enum RelationDeltaDirection {
564 Insert,
566 Delete,
568}
569
570pub struct RelationDeltaCancellation {
572 update_index: usize,
573 incoming_direction: RelationDeltaDirection,
574 tuples: CudaBuffer,
575}
576
577impl RelationDeltaCancellation {
578 pub fn update_index(&self) -> usize {
580 self.update_index
581 }
582
583 pub fn incoming_direction(&self) -> RelationDeltaDirection {
585 self.incoming_direction
586 }
587
588 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#[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 pub fn net_deltas(&self) -> &HashMap<String, RelationDelta> {
617 &self.deltas
618 }
619
620 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#[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 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 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 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#[derive(Clone)]
792struct StratumExecutable {
793 plan: StratumPlanKind,
796}
797
798#[derive(Clone)]
799struct StratifiedExecutable {
800 strata: Vec<StratumExecutable>,
801 ordinary_post: GpuOrdinaryPass,
804}
805
806#[derive(Clone)]
807enum StratumPlanKind {
808 Single(Box<EpistemicExecutablePlan>),
809 Split(Box<EpistemicSplitExecutablePlan>),
810 Ordinary {
818 plan: Box<ExecutionPlan>,
819 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 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#[derive(Clone)]
875struct EpistemicProvenance {
876 reduction: &'static str,
878 literals: Vec<xlog_ir::EirEpistemicLiteral>,
880 surface_source_queries: bool,
883}
884
885#[derive(Clone)]
887pub struct LogicProgram {
888 reusable_state_identity: Arc<LogicProgramIdentity>,
889 source_program: Program,
891 program: Program,
893 authored_constraints: Option<Vec<Constraint>>,
896 plan: LogicExecutionPlan,
897 schemas: HashMap<String, Schema>,
898 rel_ids: HashMap<String, RelId>,
899 epistemic_provenance: Option<EpistemicProvenance>,
902}
903
904#[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 pub fn name(&self) -> &str {
916 &self.name
917 }
918
919 pub fn source_named(&self) -> bool {
921 self.source_named
922 }
923
924 pub fn sort(&self) -> Option<&str> {
926 self.sort.as_deref()
927 }
928
929 pub fn scalar_type(&self) -> ScalarType {
931 self.scalar_type
932 }
933}
934
935impl LogicProgram {
936 pub fn compile(source: &str) -> Result<Self> {
938 let program = xlog_logic::parse_program(source)?;
939 Self::compile_program(program)
940 }
941
942 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 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 let provenance_literals = collect_eir_epistemic_literals(&normalized);
1070 let prepared = prepare_epistemic_program(&normalized)?;
1071 let active_program = prepared.active_program();
1072
1073 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 if let Some(stratified) = try_plan_stratified_epistemic_program(active_program)? {
1112 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 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 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 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 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 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 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 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 pub fn schema(&self, relation: &str) -> Option<&Schema> {
1409 self.schemas.get(relation)
1410 }
1411
1412 pub fn schemas(&self) -> &HashMap<String, Schema> {
1414 &self.schemas
1415 }
1416
1417 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 pub fn rule_provenance(&self) -> Vec<xlog_logic::RuleProvenance> {
1466 xlog_logic::source_diagnostics(&self.source_program, &self.program, None).0
1467 }
1468
1469 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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(), ¤t_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 ¤t_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 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 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 queries.extend(epistemic_result_to_query_results(
3943 epistemic_output_relation_name(&component.executable)?,
3944 result,
3945 ));
3946 }
3947 }
3948 LogicExecutionPlan::EpistemicStratified(stratified) => {
3949 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 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 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 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
4268pub 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 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 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 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
4731fn desugar_shared_variable_epistemic_constraints(mut program: Program) -> Program {
4759 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 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 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
4820fn 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
4832fn 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
4848fn 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
4927fn 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
5079fn 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 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 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
5234fn 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
5586fn 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
5606fn 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
5618fn 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
5635fn 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 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 unsafe { std::env::remove_var(name) };
6083 }
6084 for (name, value) in active {
6085 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 unsafe { std::env::set_var(name, value) };
6099 }
6100 None => {
6101 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}