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