Skip to main content

xlog_runtime/executor/
mod.rs

1//! Query executor for RIR nodes
2//!
3//! The executor interprets RIR (Relational IR) nodes using the CUDA kernel provider
4//! to execute GPU-accelerated relational operations.
5
6use std::collections::{HashMap, HashSet};
7use std::sync::{Arc, OnceLock};
8
9#[cfg(test)]
10use xlog_core::ScalarType;
11use xlog_core::{RelId, Result, RuntimeConfig, Schema, XlogError};
12use xlog_cuda::memory::TrackedCudaSlice;
13use xlog_cuda::{CudaBuffer, CudaKernelProvider};
14#[cfg(test)]
15use xlog_ir::{CompareOp, ConstValue, Stratum};
16use xlog_ir::{ExecutionPlan, Expr, JoinType, ProjectExpr, RirNode};
17use xlog_stats::{StatsManager, StatsSnapshot};
18
19use crate::ilp_registry::{IlpRegistry, IlpTaggedResult};
20use crate::profiler::{ExecutionStats, Profiler};
21use crate::RelationStore;
22
23mod delta;
24mod epistemic_workspace;
25mod expression;
26mod join_cache;
27mod node_dispatch;
28mod recursive;
29mod resident;
30#[cfg(all(test, feature = "resident-graph-tests"))]
31mod resident_graph_tests;
32mod rewrite;
33mod wcoj_cost_model;
34mod wcoj_dispatch;
35#[cfg(feature = "wcoj-phase-timing")]
36pub mod wcoj_phase_timing;
37pub use epistemic_workspace::{
38    EpistemicGpuBatchExecutionResult, EpistemicGpuBatchExecutionTrace,
39    EpistemicGpuCandidateGenerationTrace, EpistemicGpuCandidateValidationTrace,
40    EpistemicGpuConstraintValidationTrace, EpistemicGpuConstraintWorldViewValidationTrace,
41    EpistemicGpuExecutionResult, EpistemicGpuFinalResultMaterializationTrace,
42    EpistemicGpuFinalResultTransferTrace, EpistemicGpuFinalTupleMaterializationTrace,
43    EpistemicGpuKernelTimingTrace, EpistemicGpuMaterializationTrace,
44    EpistemicGpuModelMembershipSource, EpistemicGpuModelMembershipTrace,
45    EpistemicGpuPreparedExecution, EpistemicGpuPropagationTrace, EpistemicGpuProviderIdentity,
46    EpistemicGpuRejectionReason, EpistemicGpuRuntimeCounters, EpistemicGpuRuntimePreflight,
47    EpistemicGpuRuntimeTrace, EpistemicGpuRuntimeWcojCertification,
48    EpistemicGpuTransferBudgetTrace, EpistemicGpuWorkspace, EpistemicGpuWorkspaceCapacities,
49    EpistemicGpuWorkspaceLayout, EpistemicGpuWorkspaceResetTrace,
50    EpistemicGpuWorldViewValidationTrace,
51};
52use join_cache::JoinIndexCache;
53pub use join_cache::JoinIndexCacheStats;
54
55/// Incremental update for a base relation.
56pub struct RelationDelta {
57    /// Tuples to insert (if any).
58    pub insert: Option<CudaBuffer>,
59    /// Tuples to delete (if any).
60    pub delete: Option<CudaBuffer>,
61}
62
63impl RelationDelta {
64    /// Create a new incremental update.
65    pub fn new(insert: Option<CudaBuffer>, delete: Option<CudaBuffer>) -> Self {
66        Self { insert, delete }
67    }
68}
69
70/// Runtime summary for a delta recomputation pass.
71#[derive(Clone, Debug, Default, PartialEq, Eq)]
72pub struct DeltaRecomputeStats {
73    /// Number of base relations changed by the delta map.
74    pub changed_relations: usize,
75    /// True when at least one relation supplied delete rows.
76    pub has_deletes: bool,
77    /// Number of SCCs whose dependency closure was affected.
78    pub affected_sccs: usize,
79    /// Number of affected SCCs that were cleared and fully recomputed.
80    pub recomputed_sccs: usize,
81    /// Number of affected SCCs updated without clearing prior output.
82    pub incremental_sccs: usize,
83}
84
85/// Runtime common subexpression elimination telemetry.
86#[derive(Clone, Debug, Default, PartialEq, Eq)]
87pub struct CommonSubexpressionStats {
88    /// Number of safe subplan cache hits.
89    pub hits: u64,
90    /// Number of safe subplans evaluated and inserted into the cache.
91    pub misses: u64,
92    /// Number of subplans rejected because they cross an unsafe boundary.
93    pub unsafe_rejections: u64,
94    /// Rejection reason labels observed during the current executor lifetime.
95    pub rejection_reasons: Vec<String>,
96}
97
98/// Runtime join observation used by adaptive re-optimization decisions.
99#[derive(Clone, Debug, PartialEq)]
100pub struct AdaptiveJoinObservation {
101    /// Left relation ID observed at the join boundary.
102    pub left_rel: RelId,
103    /// Right relation ID observed at the join boundary.
104    pub right_rel: RelId,
105    /// Estimated output rows before the join executed.
106    pub estimated_output_rows: u64,
107    /// Actual output rows observed after the join executed.
108    pub actual_output_rows: u64,
109    /// Absolute row-count delta between estimate and observation.
110    pub cardinality_delta_abs: u64,
111    /// Estimated selectivity before execution.
112    pub estimated_selectivity: f64,
113    /// Actual selectivity observed after execution.
114    pub actual_selectivity: f64,
115    /// Absolute selectivity delta.
116    pub selectivity_delta_abs: f64,
117    /// Runtime heat for the left relation.
118    pub left_heat: f32,
119    /// Runtime heat for the right relation.
120    pub right_heat: f32,
121    /// Absolute heat delta between the join inputs.
122    pub heat_delta_abs: f32,
123    /// Multiplicative mis-plan ratio, always at least 1.0.
124    pub misplan_ratio: f64,
125}
126
127/// Deterministic adaptive re-optimization decision action.
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129pub enum AdaptiveReoptimizationAction {
130    /// Adaptive re-optimization is explicitly disabled.
131    Disabled,
132    /// Telemetry did not cross the deterministic adaptation threshold.
133    Skipped,
134    /// Telemetry crossed the threshold and the candidate should be attempted.
135    AttemptCandidate,
136    /// Candidate output matched the baseline and was adopted.
137    Adopted,
138    /// Candidate failed or diverged and the baseline snapshot was restored.
139    RolledBack,
140}
141
142/// Typed adaptive re-optimization decision.
143#[derive(Clone, Debug, PartialEq)]
144pub struct AdaptiveReoptimizationDecision {
145    /// Decision action.
146    pub action: AdaptiveReoptimizationAction,
147    /// Stable reason label.
148    pub reason: String,
149    /// Maximum observed mis-plan ratio used by the decision.
150    pub max_misplan_ratio: f64,
151    /// Minimum threshold required to attempt a candidate.
152    pub min_misplan_ratio: f64,
153}
154
155/// Typed adaptive re-optimization diagnostic kind.
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157pub enum AdaptiveReoptimizationDiagnosticKind {
158    /// Candidate execution returned an error.
159    CandidateExecutionFailed,
160    /// Candidate completed but did not produce baseline-equivalent outputs.
161    CandidateOutputMismatch,
162    /// Baseline relation snapshot was restored after an adverse candidate.
163    RollbackRestoredBaseline,
164}
165
166/// Typed adaptive re-optimization diagnostic.
167#[derive(Clone, Debug, PartialEq, Eq)]
168pub struct AdaptiveReoptimizationDiagnostic {
169    /// Diagnostic kind.
170    pub kind: AdaptiveReoptimizationDiagnosticKind,
171    /// Stable reason label or error text.
172    pub message: String,
173}
174
175/// Runtime adaptive re-optimization telemetry.
176#[derive(Clone, Debug, Default, PartialEq)]
177pub struct AdaptiveReoptimizationStats {
178    /// Number of adaptive entry-point invocations.
179    pub invocations: u64,
180    /// Number of invocations skipped because the feature was disabled.
181    pub disabled: u64,
182    /// Number of enabled invocations that did not attempt a candidate.
183    pub skipped: u64,
184    /// Number of candidate plans adopted.
185    pub adopted: u64,
186    /// Number of candidate plans rolled back.
187    pub rolled_back: u64,
188    /// Last deterministic decision.
189    pub last_decision: Option<AdaptiveReoptimizationDecision>,
190    /// Baseline join observations from the most recent execution.
191    pub last_observations: Vec<AdaptiveJoinObservation>,
192    /// Typed diagnostics emitted by the adaptive entry point.
193    pub diagnostics: Vec<AdaptiveReoptimizationDiagnostic>,
194    /// Tracked data-plane device-to-host calls added during the most recent adaptive path.
195    pub data_plane_dtoh_calls: u64,
196}
197
198#[derive(Clone, Debug, PartialEq, Eq, Hash)]
199enum CommonSubexpressionKey {
200    Scan {
201        rel: RelId,
202        generation: u64,
203    },
204    Filter {
205        input: Box<CommonSubexpressionKey>,
206        predicate: String,
207    },
208    ProjectChain {
209        input: Box<CommonSubexpressionKey>,
210        projections: Vec<Vec<String>>,
211    },
212    Join {
213        left: Box<CommonSubexpressionKey>,
214        right: Box<CommonSubexpressionKey>,
215        left_keys: Vec<usize>,
216        right_keys: Vec<usize>,
217    },
218    Union {
219        inputs: Vec<CommonSubexpressionKey>,
220    },
221    Distinct {
222        input: Box<CommonSubexpressionKey>,
223        key_cols: Vec<usize>,
224    },
225}
226
227/// Query executor that interprets RIR nodes using GPU kernels
228///
229/// The executor processes execution plans by iterating through strata and
230/// executing RIR node trees. It maintains a relation store for intermediate
231/// and final results.
232///
233/// # Example
234///
235/// ```ignore
236/// use std::sync::Arc;
237/// use xlog_runtime::Executor;
238/// use xlog_cuda::CudaKernelProvider;
239///
240/// let provider = Arc::new(CudaKernelProvider::new(device, memory)?);
241/// let mut executor = Executor::new(provider);
242///
243/// // Execute a plan
244/// let result = executor.execute_plan(&plan)?;
245/// ```
246pub struct Executor {
247    /// Stable identity for optimistic resident transactions. Unlike the
248    /// address of `self`, this token survives moves of the executor value.
249    transaction_identity: Arc<()>,
250    /// CUDA kernel provider for GPU operations
251    provider: Arc<CudaKernelProvider>,
252    /// Storage for named relations
253    store: RelationStore,
254    /// Mapping from RelId to relation name
255    rel_names: HashMap<RelId, String>,
256    /// Mapping from relation name to RelId
257    name_to_rel: HashMap<String, RelId>,
258    /// Runtime statistics for adaptive optimization
259    stats: StatsManager,
260    /// Cached build-side join indexes (adaptive indexing)
261    join_index_cache: JoinIndexCache,
262    /// Per-execution CSE cache for safe deterministic subplans.
263    common_subexpression_cache: HashMap<CommonSubexpressionKey, CudaBuffer>,
264    /// Runtime CSE telemetry for evidence and diagnostics.
265    common_subexpression_stats: CommonSubexpressionStats,
266    /// Runtime adaptive re-optimization telemetry for adoption/rollback evidence.
267    adaptive_reoptimization_stats: AdaptiveReoptimizationStats,
268    /// Per-plan join observations captured before `StatsManager` is updated.
269    adaptive_join_observations: Vec<AdaptiveJoinObservation>,
270    /// Runtime configuration
271    config: RuntimeConfig,
272    /// Performance profiler for --stats output
273    profiler: Profiler,
274    /// ILP tensor mask registry
275    ilp_registry: IlpRegistry,
276    /// Last ILP tagged result metadata
277    ilp_last_result: Option<IlpTaggedResult>,
278    /// Number of times the env-gated WCOJ triangle dispatch
279    /// (`XLOG_USE_WCOJ_TRIANGLE_U32` / `RuntimeConfig::wcoj_triangle_dispatch`)
280    /// produced a result and the executor installed it. Tests use this
281    /// counter to assert that the WCOJ path actually fired vs. silently
282    /// falling back to the binary-join chain with the same answer.
283    wcoj_triangle_dispatch_count: u64,
284    /// Count of times `try_dispatch_wcoj_4cycle` produced a result and the
285    /// executor installed it. Tracks 4-cycle dispatches separately from triangle.
286    pub(super) wcoj_4cycle_dispatch_count: u64,
287    /// Count of times the chain dispatcher produced a result and the
288    /// executor installed it.
289    pub(super) chain_dispatch_count: u64,
290    /// Scan operations in embedded binary fallbacks whose ChainJoin
291    /// specialization completed successfully. These are logical
292    /// equivalents, not physically executed profiler operations.
293    pub(super) chain_fallback_scan_equivalents: u64,
294    /// Filter operations in embedded binary fallbacks whose ChainJoin
295    /// specialization completed successfully. These are logical
296    /// equivalents, not physically executed profiler operations.
297    pub(super) chain_fallback_filter_equivalents: u64,
298    /// Count of times `try_dispatch_wcoj_clique5` produced
299    /// a result and the executor installed it. Public accessor:
300    /// `Executor::wcoj_clique5_dispatch_count(&self) -> u64`.
301    pub(super) wcoj_clique5_dispatch_count: u64,
302    /// Count of times `try_dispatch_wcoj_clique6` produced
303    /// a result and the executor installed it.
304    pub(super) wcoj_clique6_dispatch_count: u64,
305    /// Count of times `try_dispatch_wcoj_clique7` produced
306    /// a result and the executor installed it.
307    pub(super) wcoj_clique7_dispatch_count: u64,
308    /// Count of times `try_dispatch_wcoj_clique8` produced
309    /// a result and the executor installed it.
310    pub(super) wcoj_clique8_dispatch_count: u64,
311    /// Number of recursive Merge-phase K-clique histogram refresh
312    /// boundaries observed.
313    pub(super) kclique_histogram_refresh_count: u64,
314    /// Cumulative nanoseconds spent in recursive Merge-phase K-clique
315    /// histogram refresh accounting.
316    pub(super) kclique_histogram_refresh_nanos: u128,
317    /// Count of times `execute_join` routed an inner-join to the
318    /// nested-loop provider entry point
319    /// (`CudaKernelProvider::nested_loop_join_v2_inner_u32_1key`)
320    /// because the eligibility predicate + Cartesian-product
321    /// threshold both held. Tests use this counter to assert that
322    /// the nested-loop path actually fired vs. silently falling back to
323    /// hash. Public accessor:
324    /// `Executor::nested_loop_dispatch_count(&self) -> u64`.
325    pub(super) nested_loop_dispatch_count: u64,
326    /// Counts WCOJ pipeline errors (layout or kernel failures) that were
327    /// converted into binary-join declines instead of propagating. Healthy
328    /// dispatch keeps this at 0; nonzero values expose regressed kernels
329    /// that would otherwise hide behind the silent-fallback contract.
330    /// `XLOG_WCOJ_STRICT=1` propagates the error instead. Public accessor:
331    /// `Executor::wcoj_error_decline_count(&self) -> u64`.
332    pub(super) wcoj_error_decline_count: u64,
333    /// Count of fused group-by-root count dispatches that produced the
334    /// installed result. Public accessor:
335    /// `Executor::wcoj_groupby_fusion_dispatch_count(&self) -> u64`.
336    pub(super) wcoj_groupby_fusion_dispatch_count: u64,
337    /// Count of generalized Free Join dispatches that produced the
338    /// installed result. Public accessor:
339    /// `Executor::free_join_dispatch_count(&self) -> u64`.
340    pub(super) free_join_dispatch_count: u64,
341    /// D3 — count of factorized recursive-delta dispatches that produced
342    /// an installed novel set. Public accessor:
343    /// `Executor::factorized_delta_dispatch_count(&self) -> u64`.
344    pub(super) factorized_delta_dispatch_count: u64,
345    /// Cached non-default stream for the WCOJ triangle dispatch hook.
346    /// Acquired lazily on first dispatch and reused thereafter — mirrors
347    /// [`xlog_cuda::CudaKernelProvider::recorded_op_stream`] for the
348    /// same reason: the device-runtime
349    /// [`xlog_cuda::device_runtime::StreamPool`] is grow-only with a
350    /// hard cap (default 16). Acquiring per-invocation would silently
351    /// drain the pool on long-lived runtimes (benchmarks, soak tests,
352    /// any program with >16 matching WCOJ-eligible rules) and route
353    /// subsequent dispatches through the binary-join fallback,
354    /// invalidating the dispatch counter and the gate-on path.
355    ///
356    /// **Shared across WCOJ shapes**: triangle and 4-cycle dispatch both
357    /// acquire and reuse this single stream. Renamed from
358    /// `wcoj_triangle_stream` when 4-cycle dispatch landed.
359    wcoj_dispatch_stream: OnceLock<xlog_cuda::device_runtime::StreamId>,
360    /// Stable non-default stream used to instantiate and replay resident
361    /// conditional graphs. The runtime stream pool is grow-only, so acquiring
362    /// once per executor prevents long-lived evaluations from draining it.
363    resident_graph_stream: OnceLock<xlog_cuda::device_runtime::StreamId>,
364    /// Diagnostic-only: per-dispatch WCOJ triangle phase
365    /// timings, populated by `try_dispatch_wcoj_triangle` when
366    /// the `wcoj-phase-timing` Cargo feature is on. Read by the
367    /// `wcoj_phase_report` binary in xlog-integration. Field is
368    /// absent under feature-off so production builds have zero
369    /// overhead.
370    #[cfg(feature = "wcoj-phase-timing")]
371    pub(super) last_wcoj_phase_timing:
372        std::sync::Mutex<Option<wcoj_phase_timing::WcojDispatchPhaseTiming>>,
373    /// Per-iteration recursive-SCC stats trace, populated by
374    /// `execute_recursive_scc` after each delta-relation and full-relation
375    /// cardinality update site. Field, types, accessor, and populating call
376    /// sites are gated on the `recursive-stats-trace` Cargo feature
377    /// (default OFF) so production builds carry zero trace overhead: no
378    /// field, no populating call site, no symbol. The recursive stats trace
379    /// test target declares this feature in its `required-features`, so it
380    /// is only built when the feature is enabled.
381    #[cfg(feature = "recursive-stats-trace")]
382    pub(super) last_recursive_stats_trace: RecursiveStatsTrace,
383}
384
385/// Recursive-SCC stats trace for recursive cardinality updates.
386///
387/// Captures one entry per `(iteration, predicate)` boundary
388/// at which `execute_recursive_scc` updates `StatsManager` for
389/// a recursive predicate's `(full_rel, delta_rel)` RelIds.
390/// Used by recursive stats trace tests to assert per-iteration cardinality
391/// evolution and binary-join estimate evolution without intrusive
392/// instrumentation.
393#[cfg(feature = "recursive-stats-trace")]
394#[derive(Debug, Default, Clone)]
395#[allow(missing_docs)]
396pub struct RecursiveStatsTrace {
397    pub entries: Vec<RecursiveStatsTraceEntry>,
398}
399
400/// One entry per `(iteration, pred)` boundary.
401///
402/// `iteration == 0` is the seed pass; `iteration >= 1` is the
403/// fixpoint loop. `phase` distinguishes the delta-recording site
404/// from the full-recording site so full-row growth assertions only
405/// see snapshots where `full_rel` advanced, and delta-evolution
406/// assertions only see delta-update snapshots.
407#[cfg(feature = "recursive-stats-trace")]
408#[derive(Debug, Clone)]
409#[allow(missing_docs)]
410pub struct RecursiveStatsTraceEntry {
411    pub iteration: usize,
412    pub pred: String,
413    pub full_rel: RelId,
414    pub delta_rel: RelId,
415    pub full_rows: u64,
416    pub delta_rows: u64,
417    pub phase: RecursiveStatsPhase,
418    /// Optional binary-join estimate the cost model would use
419    /// for the variant body's first binary hop. Triangle:
420    /// `(delta_e1_rel, e2_rel, &[1], &[0])`. 4-cycle: same
421    /// `(delta_e1_rel, e2_rel, &[1], &[0])` (slot 0 → slot 1
422    /// adjacency on the X variable).
423    pub binary_est_for_variant: Option<u64>,
424}
425
426#[cfg(feature = "recursive-stats-trace")]
427#[derive(Debug, Clone, Copy, PartialEq, Eq)]
428#[allow(missing_docs)]
429pub enum RecursiveStatsPhase {
430    /// Seed pass — full_rel + delta_rel both updated; trace
431    /// entry contains both row counts. iteration == 0.
432    Seed,
433    /// Fixpoint loop delta update: `delta_rel` updated while `full_rel`
434    /// holds the previous iteration's value. Trace entry reports
435    /// `full_rows` as the previous-iteration cardinality it sees.
436    Phase2Delta,
437    /// Fixpoint loop full update after merge. Trace entry reports the new
438    /// full row count plus the `delta_rel` value just recorded by the
439    /// delta update.
440    Phase4Full,
441}
442
443impl Executor {
444    /// Create a new executor with the given kernel provider
445    ///
446    /// # Arguments
447    /// * `provider` - The CUDA kernel provider for GPU operations
448    pub fn new(provider: Arc<CudaKernelProvider>) -> Self {
449        Self::new_with_config(provider, RuntimeConfig::default())
450    }
451
452    /// Create a new executor with the given kernel provider and runtime config
453    pub fn new_with_config(provider: Arc<CudaKernelProvider>, config: RuntimeConfig) -> Self {
454        const DEFAULT_JOIN_INDEX_CACHE_BYTES: u64 = 256 * 1024 * 1024;
455        let max_index_cache_bytes =
456            (provider.memory().budget().device_bytes / 4).min(DEFAULT_JOIN_INDEX_CACHE_BYTES);
457        Self {
458            transaction_identity: Arc::new(()),
459            provider: provider.clone(),
460            store: RelationStore::new(provider.clone()),
461            rel_names: HashMap::new(),
462            name_to_rel: HashMap::new(),
463            stats: StatsManager::new(),
464            join_index_cache: JoinIndexCache::new(max_index_cache_bytes),
465            common_subexpression_cache: HashMap::new(),
466            common_subexpression_stats: CommonSubexpressionStats::default(),
467            adaptive_reoptimization_stats: AdaptiveReoptimizationStats::default(),
468            adaptive_join_observations: Vec::new(),
469            config,
470            profiler: Profiler::default(),
471            ilp_registry: IlpRegistry::new(),
472            ilp_last_result: None,
473            wcoj_triangle_dispatch_count: 0,
474            wcoj_4cycle_dispatch_count: 0,
475            chain_dispatch_count: 0,
476            chain_fallback_scan_equivalents: 0,
477            chain_fallback_filter_equivalents: 0,
478            wcoj_clique5_dispatch_count: 0,
479            wcoj_clique6_dispatch_count: 0,
480            wcoj_clique7_dispatch_count: 0,
481            wcoj_clique8_dispatch_count: 0,
482            kclique_histogram_refresh_count: 0,
483            kclique_histogram_refresh_nanos: 0,
484            nested_loop_dispatch_count: 0,
485            wcoj_error_decline_count: 0,
486            wcoj_groupby_fusion_dispatch_count: 0,
487            free_join_dispatch_count: 0,
488            factorized_delta_dispatch_count: 0,
489            wcoj_dispatch_stream: OnceLock::new(),
490            resident_graph_stream: OnceLock::new(),
491            #[cfg(feature = "wcoj-phase-timing")]
492            last_wcoj_phase_timing: std::sync::Mutex::new(None),
493            #[cfg(feature = "recursive-stats-trace")]
494            last_recursive_stats_trace: RecursiveStatsTrace::default(),
495        }
496    }
497
498    /// Return the most recent recursive-SCC stats trace populated by
499    /// `execute_recursive_scc`. Gated on the `recursive-stats-trace`
500    /// Cargo feature; default OFF.
501    #[cfg(feature = "recursive-stats-trace")]
502    pub fn last_recursive_stats_trace(&self) -> &RecursiveStatsTrace {
503        &self.last_recursive_stats_trace
504    }
505
506    /// Take the most recent WCOJ triangle dispatch's per-phase
507    /// timing breakdown. Reading clears the slot — designed for
508    /// one-shot consumption by the `wcoj_phase_report` binary.
509    /// Returns `None` if no triangle has dispatched since the
510    /// last read (or since construction).
511    ///
512    /// Compiled in only with the `wcoj-phase-timing` Cargo
513    /// feature; production builds have no such method.
514    #[cfg(feature = "wcoj-phase-timing")]
515    pub fn take_wcoj_phase_timing(&self) -> Option<wcoj_phase_timing::WcojDispatchPhaseTiming> {
516        self.last_wcoj_phase_timing
517            .lock()
518            .ok()
519            .and_then(|mut g| g.take())
520    }
521
522    /// Enable or disable the performance profiler
523    ///
524    /// When enabled, execution statistics will be collected for --stats output.
525    pub fn set_profiling(&mut self, enabled: bool) {
526        self.profiler = Profiler::new(enabled);
527        if enabled {
528            self.profiler
529                .set_memory_budget(self.provider.memory().budget().device_bytes);
530        }
531    }
532
533    /// Check if profiling is enabled
534    pub fn is_profiling(&self) -> bool {
535        self.profiler.is_enabled()
536    }
537
538    /// Get execution statistics
539    ///
540    /// Returns collected statistics if profiling was enabled. Peak memory is
541    /// the provider memory manager's reservation high-water mark, so it spans
542    /// the provider lifetime rather than a single execution.
543    pub fn execution_stats(&self, total_output_rows: u64) -> ExecutionStats {
544        let mut stats = self.profiler.execution_stats(total_output_rows);
545        stats.peak_memory_bytes = self.provider.memory().peak_bytes();
546        // WCOJ/multiway dispatch counters live on the executor, not the
547        // profiler. Surface them so a `--stats` run can be *verified* to have
548        // used the WCOJ kernels rather than silently falling back to binary
549        // joins (the failure mode that wasted a full GPU benchmark cycle).
550        stats.wcoj_triangle_dispatch_count = self.wcoj_triangle_dispatch_count();
551        stats.wcoj_4cycle_dispatch_count = self.wcoj_4cycle_dispatch_count();
552        stats.wcoj_groupby_fusion_dispatch_count = self.wcoj_groupby_fusion_dispatch_count();
553        stats.free_join_dispatch_count = self.free_join_dispatch_count();
554        stats.factorized_delta_dispatch_count = self.factorized_delta_dispatch_count();
555        stats.wcoj_error_decline_count = self.wcoj_error_decline_count();
556        stats.chain_fallback_scan_equivalents = self.chain_fallback_scan_equivalents;
557        stats.chain_fallback_filter_equivalents = self.chain_fallback_filter_equivalents;
558        stats
559    }
560
561    /// Get a reference to the relation store
562    pub fn store(&self) -> &RelationStore {
563        &self.store
564    }
565
566    /// Get a mutable reference to the relation store
567    pub fn store_mut(&mut self) -> &mut RelationStore {
568        &mut self.store
569    }
570
571    /// Get a mutable reference to the ILP registry.
572    pub fn ilp_registry_mut(&mut self) -> &mut IlpRegistry {
573        &mut self.ilp_registry
574    }
575
576    /// Get a shared reference to the ILP registry.
577    pub fn ilp_registry(&self) -> &IlpRegistry {
578        &self.ilp_registry
579    }
580
581    /// Get the last ILP tagged result.
582    pub fn ilp_last_result(&self) -> Option<&IlpTaggedResult> {
583        self.ilp_last_result.as_ref()
584    }
585
586    /// Store a relation buffer and invalidate join indices.
587    pub fn put_relation(&mut self, name: &str, buffer: CudaBuffer) {
588        self.store_put(name, buffer);
589    }
590
591    /// Get a reference to the runtime statistics manager
592    pub fn stats(&self) -> &StatsManager {
593        &self.stats
594    }
595
596    /// Return persistent join-index manager telemetry.
597    pub fn join_index_cache_stats(&self) -> JoinIndexCacheStats {
598        self.join_index_cache.stats()
599    }
600
601    /// Reset executor state for Monte Carlo sampling.
602    ///
603    /// Clears relation storage and join index cache while preserving relation registrations.
604    pub fn reset_for_mc(&mut self) {
605        self.store.clear();
606        self.join_index_cache.clear();
607        self.common_subexpression_cache.clear();
608        self.adaptive_join_observations.clear();
609    }
610
611    /// Targeted MC reset: preserve base/static relations and clear dynamic ones.
612    ///
613    /// Unlike [`Self::reset_for_mc`] which drops all relations, this method keeps the
614    /// relations listed in `preserve` untouched, removes every other relation,
615    /// then re-creates the relations specified in `clear_to_empty` as empty
616    /// GPU buffers with the given schemas.  The join-index cache is fully
617    /// invalidated because dynamic relations have changed.
618    ///
619    /// # Arguments
620    /// * `preserve` - Relation names to keep as-is (base/static facts).
621    /// * `clear_to_empty` - `(name, schema)` pairs for dynamic relations that
622    ///   should be present but empty after the reset.
623    pub fn reset_for_mc_relations(
624        &mut self,
625        preserve: &[&str],
626        clear_to_empty: &[(&str, Schema)],
627    ) -> Result<()> {
628        let preserve_set: HashSet<&str> = preserve.iter().copied().collect();
629        let existing_names: Vec<String> = self.store.names().map(|s| s.to_string()).collect();
630
631        for name in &existing_names {
632            if !preserve_set.contains(name.as_str()) {
633                self.store.remove(name);
634            }
635        }
636
637        for (name, schema) in clear_to_empty {
638            let empty = self.provider.create_empty_buffer(schema.clone())?;
639            self.store.put(name, empty);
640        }
641
642        self.join_index_cache.clear();
643        self.common_subexpression_cache.clear();
644        self.adaptive_join_observations.clear();
645        Ok(())
646    }
647
648    /// Reset executor state for ILP attempt reuse.
649    ///
650    /// Clears ILP registry (masks + tagged results), relation storage,
651    /// join index cache, stats, and profiler. Preserves relation name
652    /// registrations (rel_names, name_to_rel) since those are immutable
653    /// compile artifacts.
654    pub fn reset_for_ilp(&mut self) {
655        self.ilp_registry.clear();
656        self.ilp_last_result = None;
657        self.store.clear();
658        self.join_index_cache.clear();
659        self.common_subexpression_cache.clear();
660        self.adaptive_join_observations.clear();
661        self.stats = StatsManager::new();
662        self.profiler = Profiler::default();
663    }
664
665    /// Get a mutable reference to the runtime statistics manager
666    pub fn stats_mut(&mut self) -> &mut StatsManager {
667        &mut self.stats
668    }
669
670    /// Capture a runtime statistics snapshot, including predicate name mappings.
671    ///
672    /// Use this snapshot to seed the compiler/optimizer on subsequent compilations.
673    pub fn stats_snapshot(&self) -> StatsSnapshot {
674        let mut snapshot = self.stats.snapshot();
675        snapshot.rel_names = self
676            .rel_names
677            .iter()
678            .map(|(id, name)| (*id, name.clone()))
679            .collect();
680        snapshot
681    }
682
683    /// Return runtime CSE telemetry for evidence and diagnostics.
684    pub fn common_subexpression_stats(&self) -> &CommonSubexpressionStats {
685        &self.common_subexpression_stats
686    }
687
688    /// Return adaptive re-optimization telemetry for evidence and diagnostics.
689    pub fn adaptive_reoptimization_stats(&self) -> &AdaptiveReoptimizationStats {
690        &self.adaptive_reoptimization_stats
691    }
692
693    /// Replay the deterministic adaptive decision against captured telemetry.
694    pub fn replay_adaptive_reoptimization_decision(
695        &self,
696        observations: &[AdaptiveJoinObservation],
697    ) -> AdaptiveReoptimizationDecision {
698        self.adaptive_reoptimization_decision(observations)
699    }
700
701    fn common_subexpression_enabled(&self) -> bool {
702        self.config.resolved_common_subexpression_elimination()
703    }
704
705    fn adaptive_reoptimization_enabled(&self) -> bool {
706        self.config.resolved_adaptive_reoptimization()
707    }
708
709    fn adaptive_reoptimization_decision(
710        &self,
711        observations: &[AdaptiveJoinObservation],
712    ) -> AdaptiveReoptimizationDecision {
713        let min_misplan_ratio = self
714            .config
715            .resolved_adaptive_reoptimization_min_misplan_ratio();
716        let max_misplan_ratio = observations
717            .iter()
718            .map(|observation| observation.misplan_ratio)
719            .fold(1.0_f64, f64::max);
720
721        if !self.adaptive_reoptimization_enabled() {
722            return AdaptiveReoptimizationDecision {
723                action: AdaptiveReoptimizationAction::Disabled,
724                reason: "adaptive_reoptimization_disabled".to_string(),
725                max_misplan_ratio,
726                min_misplan_ratio,
727            };
728        }
729
730        if observations.is_empty() {
731            return AdaptiveReoptimizationDecision {
732                action: AdaptiveReoptimizationAction::Skipped,
733                reason: "no_join_telemetry".to_string(),
734                max_misplan_ratio,
735                min_misplan_ratio,
736            };
737        }
738
739        if max_misplan_ratio >= min_misplan_ratio {
740            AdaptiveReoptimizationDecision {
741                action: AdaptiveReoptimizationAction::AttemptCandidate,
742                reason: "misplan_threshold_crossed".to_string(),
743                max_misplan_ratio,
744                min_misplan_ratio,
745            }
746        } else {
747            AdaptiveReoptimizationDecision {
748                action: AdaptiveReoptimizationAction::Skipped,
749                reason: "misplan_threshold_not_crossed".to_string(),
750                max_misplan_ratio,
751                min_misplan_ratio,
752            }
753        }
754    }
755
756    fn record_adaptive_join_observation(
757        &mut self,
758        left_rel: RelId,
759        right_rel: RelId,
760        left_keys: &[usize],
761        right_keys: &[usize],
762        input_rows: u64,
763        actual_output_rows: u64,
764    ) {
765        let estimated_output_rows = self
766            .stats
767            .estimate_join_cardinality(left_rel, right_rel, left_keys, right_keys);
768        let estimated_selectivity = if input_rows > 0 {
769            estimated_output_rows as f64 / input_rows as f64
770        } else {
771            0.0
772        };
773        let actual_selectivity = if input_rows > 0 {
774            actual_output_rows as f64 / input_rows as f64
775        } else {
776            0.0
777        };
778        let cardinality_delta_abs = estimated_output_rows.abs_diff(actual_output_rows);
779        let selectivity_delta_abs = (estimated_selectivity - actual_selectivity).abs();
780        let left_heat = self
781            .stats
782            .get_relation_stats(left_rel)
783            .map(|stats| stats.heat)
784            .unwrap_or(0.0);
785        let right_heat = self
786            .stats
787            .get_relation_stats(right_rel)
788            .map(|stats| stats.heat)
789            .unwrap_or(0.0);
790        let heat_delta_abs = (left_heat - right_heat).abs();
791        let smaller = estimated_output_rows.min(actual_output_rows);
792        let larger = estimated_output_rows.max(actual_output_rows);
793        let misplan_ratio = if smaller == 0 {
794            if larger == 0 {
795                1.0
796            } else {
797                f64::INFINITY
798            }
799        } else {
800            (larger as f64 / smaller as f64).max(1.0)
801        };
802
803        self.adaptive_join_observations
804            .push(AdaptiveJoinObservation {
805                left_rel,
806                right_rel,
807                estimated_output_rows,
808                actual_output_rows,
809                cardinality_delta_abs,
810                estimated_selectivity,
811                actual_selectivity,
812                selectivity_delta_abs,
813                left_heat,
814                right_heat,
815                heat_delta_abs,
816                misplan_ratio,
817            });
818    }
819
820    fn plan_head_names(plan: &ExecutionPlan) -> Vec<String> {
821        let mut names = Vec::new();
822        for stratum in &plan.strata {
823            for scc_id in &stratum.sccs {
824                if let Some(rules) = plan.rules_by_scc.get(*scc_id as usize) {
825                    for rule in rules {
826                        if !names.iter().any(|name| name == &rule.head) {
827                            names.push(rule.head.clone());
828                        }
829                    }
830                }
831            }
832        }
833
834        if names.is_empty() {
835            for rules in &plan.rules_by_scc {
836                for rule in rules {
837                    if !names.iter().any(|name| name == &rule.head) {
838                        names.push(rule.head.clone());
839                    }
840                }
841            }
842        }
843
844        names
845    }
846
847    fn clone_store_snapshot(&self) -> Result<HashMap<String, CudaBuffer>> {
848        let names: Vec<String> = self.store.names().map(|name| name.to_string()).collect();
849        let mut snapshot = HashMap::with_capacity(names.len());
850        for name in names {
851            if let Some(buffer) = self.store.get(&name) {
852                snapshot.insert(name, self.clone_buffer(buffer)?);
853            }
854        }
855        Ok(snapshot)
856    }
857
858    fn restore_store_snapshot(&mut self, snapshot: HashMap<String, CudaBuffer>) {
859        let snapshot_names: HashSet<String> = snapshot.keys().cloned().collect();
860        let existing_names: Vec<String> = self.store.names().map(|name| name.to_string()).collect();
861        for name in existing_names {
862            if !snapshot_names.contains(&name) {
863                self.store.remove(&name);
864            }
865        }
866        for (name, buffer) in snapshot {
867            self.store.put(&name, buffer);
868        }
869    }
870
871    fn restore_stats_snapshot(&mut self, snapshot: &StatsSnapshot) {
872        self.stats.clear();
873        self.stats.merge_snapshot(snapshot);
874    }
875
876    fn clone_final_plan_output(&self, plan: &ExecutionPlan) -> Result<CudaBuffer> {
877        let head_names = Self::plan_head_names(plan);
878        if let Some(name) = head_names.last() {
879            let output = self.store.get(name).ok_or_else(|| {
880                XlogError::Execution(format!("adaptive reoptimization output missing: {name}"))
881            })?;
882            return self.clone_buffer(output);
883        }
884
885        self.provider.create_empty_buffer(Schema::new(vec![]))
886    }
887
888    fn plan_outputs_match(
889        &self,
890        head_names: &[String],
891        baseline_snapshot: &HashMap<String, CudaBuffer>,
892    ) -> Result<bool> {
893        for name in head_names {
894            let Some(baseline) = baseline_snapshot.get(name) else {
895                return Ok(false);
896            };
897            let Some(candidate) = self.store.get(name) else {
898                return Ok(false);
899            };
900            if !self.buffers_gpu_set_equivalent(baseline, candidate)? {
901                return Ok(false);
902            }
903        }
904        Ok(true)
905    }
906
907    fn buffers_gpu_set_equivalent(&self, left: &CudaBuffer, right: &CudaBuffer) -> Result<bool> {
908        if left.schema() != right.schema() {
909            return Ok(false);
910        }
911        let left_rows = self.provider.device_row_count(left)?;
912        let right_rows = self.provider.device_row_count(right)?;
913        if left_rows != right_rows {
914            return Ok(false);
915        }
916
917        let left_minus_right = self.provider.diff_full_row(left, right)?;
918        if self.provider.device_row_count(&left_minus_right)? != 0 {
919            return Ok(false);
920        }
921        let right_minus_left = self.provider.diff_full_row(right, left)?;
922        Ok(self.provider.device_row_count(&right_minus_left)? == 0)
923    }
924
925    fn record_adaptive_dtoh_delta(&mut self, before_dtoh_calls: u64) {
926        let after_dtoh_calls = self.provider.host_transfer_stats().dtoh_calls;
927        self.adaptive_reoptimization_stats.data_plane_dtoh_calls =
928            after_dtoh_calls.saturating_sub(before_dtoh_calls);
929    }
930
931    fn is_common_subexpression_cacheable(node: &RirNode) -> bool {
932        !matches!(node, RirNode::Unit | RirNode::Scan { .. })
933    }
934
935    fn record_common_subexpression_rejection(&mut self, reason: &'static str) {
936        self.common_subexpression_stats.unsafe_rejections = self
937            .common_subexpression_stats
938            .unsafe_rejections
939            .saturating_add(1);
940        if !self
941            .common_subexpression_stats
942            .rejection_reasons
943            .iter()
944            .any(|seen| seen == reason)
945        {
946            self.common_subexpression_stats
947                .rejection_reasons
948                .push(reason.to_string());
949        }
950    }
951
952    fn common_subexpression_key(&mut self, node: &RirNode) -> Option<CommonSubexpressionKey> {
953        match node {
954            RirNode::Unit => None,
955            RirNode::Scan { rel } => {
956                let generation = self
957                    .get_rel_name(*rel)
958                    .and_then(|name| self.store.version(name))
959                    .unwrap_or(0);
960                Some(CommonSubexpressionKey::Scan {
961                    rel: *rel,
962                    generation,
963                })
964            }
965            RirNode::Filter { input, predicate } => {
966                let input = self.common_subexpression_key(input)?;
967                Some(CommonSubexpressionKey::Filter {
968                    input: Box::new(input),
969                    predicate: Self::expr_cse_key(predicate),
970                })
971            }
972            RirNode::Project { .. } => self.common_subexpression_project_chain_key(node),
973            RirNode::Join {
974                left,
975                right,
976                left_keys,
977                right_keys,
978                join_type,
979            } => {
980                if *join_type != JoinType::Inner {
981                    self.record_common_subexpression_rejection("negation_or_outer_join_boundary");
982                    return None;
983                }
984                let left = self.common_subexpression_key(left)?;
985                let right = self.common_subexpression_key(right)?;
986                Some(CommonSubexpressionKey::Join {
987                    left: Box::new(left),
988                    right: Box::new(right),
989                    left_keys: left_keys.clone(),
990                    right_keys: right_keys.clone(),
991                })
992            }
993            RirNode::Union { inputs } => {
994                let mut input_keys = Vec::with_capacity(inputs.len());
995                for input in inputs {
996                    input_keys.push(self.common_subexpression_key(input)?);
997                }
998                Some(CommonSubexpressionKey::Union { inputs: input_keys })
999            }
1000            RirNode::Distinct { input, key_cols } => {
1001                let input = self.common_subexpression_key(input)?;
1002                Some(CommonSubexpressionKey::Distinct {
1003                    input: Box::new(input),
1004                    key_cols: key_cols.clone(),
1005                })
1006            }
1007            RirNode::Diff { .. } => {
1008                self.record_common_subexpression_rejection("negation_or_difference_boundary");
1009                None
1010            }
1011            RirNode::GroupBy { .. } => {
1012                self.record_common_subexpression_rejection("aggregate_boundary");
1013                None
1014            }
1015            RirNode::Fixpoint { .. } => {
1016                self.record_common_subexpression_rejection("recursive_or_mutable_boundary");
1017                None
1018            }
1019            RirNode::TensorMaskedJoin { .. } => {
1020                self.record_common_subexpression_rejection("provenance_or_tensor_boundary");
1021                None
1022            }
1023            RirNode::MultiWayJoin { .. } | RirNode::ChainJoin { .. } => {
1024                self.record_common_subexpression_rejection("specialized_dispatch_boundary");
1025                None
1026            }
1027        }
1028    }
1029
1030    fn common_subexpression_project_chain_key(
1031        &mut self,
1032        node: &RirNode,
1033    ) -> Option<CommonSubexpressionKey> {
1034        // Treat a consecutive projection chain as one cacheable subplan. Predicate-bodied
1035        // function expansion can produce widening chains, and retaining every prefix would
1036        // keep every intermediate GPU buffer alive for the rest of the execution.
1037        let mut project_columns = Vec::new();
1038        let mut base = node;
1039        while let RirNode::Project { input, columns } = base {
1040            project_columns.push(columns);
1041            base = input;
1042        }
1043
1044        let input = Box::new(self.common_subexpression_key(base)?);
1045        let projections = project_columns
1046            .into_iter()
1047            .rev()
1048            .map(|columns| columns.iter().map(Self::project_expr_cse_key).collect())
1049            .collect();
1050
1051        Some(CommonSubexpressionKey::ProjectChain { input, projections })
1052    }
1053
1054    fn expr_cse_key(expr: &Expr) -> String {
1055        enum KeyTask<'a> {
1056            Expression(&'a Expr),
1057            FinishUnary(&'static str),
1058            FinishBinary(&'static str),
1059            FinishComparison(xlog_ir::CompareOp),
1060            FinishList {
1061                prefix: &'static str,
1062                item_count: usize,
1063            },
1064            FinishCast(xlog_core::ScalarType),
1065            FinishConditional,
1066        }
1067
1068        fn schedule_binary<'a>(
1069            tasks: &mut Vec<KeyTask<'a>>,
1070            left: &'a Expr,
1071            right: &'a Expr,
1072            prefix: &'static str,
1073        ) {
1074            tasks.push(KeyTask::FinishBinary(prefix));
1075            tasks.push(KeyTask::Expression(right));
1076            tasks.push(KeyTask::Expression(left));
1077        }
1078
1079        let mut tasks = vec![KeyTask::Expression(expr)];
1080        let mut values = Vec::new();
1081        while let Some(task) = tasks.pop() {
1082            match task {
1083                KeyTask::Expression(expression) => match expression {
1084                    Expr::Column(index) => values.push(format!("col:{index}")),
1085                    Expr::Const(value) => {
1086                        values.push(format!("const:{}", Self::const_cse_key(value)));
1087                    }
1088                    Expr::Compare { left, op, right } => {
1089                        tasks.push(KeyTask::FinishComparison(*op));
1090                        tasks.push(KeyTask::Expression(right));
1091                        tasks.push(KeyTask::Expression(left));
1092                    }
1093                    Expr::And(items) => {
1094                        tasks.push(KeyTask::FinishList {
1095                            prefix: "and",
1096                            item_count: items.len(),
1097                        });
1098                        for item in items.iter().rev() {
1099                            tasks.push(KeyTask::Expression(item));
1100                        }
1101                    }
1102                    Expr::Or(items) => {
1103                        tasks.push(KeyTask::FinishList {
1104                            prefix: "or",
1105                            item_count: items.len(),
1106                        });
1107                        for item in items.iter().rev() {
1108                            tasks.push(KeyTask::Expression(item));
1109                        }
1110                    }
1111                    Expr::Not(inner) => {
1112                        tasks.push(KeyTask::FinishUnary("not"));
1113                        tasks.push(KeyTask::Expression(inner));
1114                    }
1115                    Expr::Add(left, right) => schedule_binary(&mut tasks, left, right, "add"),
1116                    Expr::Sub(left, right) => schedule_binary(&mut tasks, left, right, "sub"),
1117                    Expr::Mul(left, right) => schedule_binary(&mut tasks, left, right, "mul"),
1118                    Expr::Div(left, right) => schedule_binary(&mut tasks, left, right, "div"),
1119                    Expr::Mod(left, right) => schedule_binary(&mut tasks, left, right, "mod"),
1120                    Expr::Abs(inner) => {
1121                        tasks.push(KeyTask::FinishUnary("abs"));
1122                        tasks.push(KeyTask::Expression(inner));
1123                    }
1124                    Expr::Min(left, right) => schedule_binary(&mut tasks, left, right, "min"),
1125                    Expr::Max(left, right) => schedule_binary(&mut tasks, left, right, "max"),
1126                    Expr::Pow(left, right) => schedule_binary(&mut tasks, left, right, "pow"),
1127                    Expr::Cast(inner, target) => {
1128                        tasks.push(KeyTask::FinishCast(*target));
1129                        tasks.push(KeyTask::Expression(inner));
1130                    }
1131                    Expr::Conditional {
1132                        condition,
1133                        then_expr,
1134                        else_expr,
1135                    } => {
1136                        tasks.push(KeyTask::FinishConditional);
1137                        tasks.push(KeyTask::Expression(else_expr));
1138                        tasks.push(KeyTask::Expression(then_expr));
1139                        tasks.push(KeyTask::Expression(condition));
1140                    }
1141                },
1142                KeyTask::FinishUnary(prefix) => {
1143                    let value = values.pop().expect("unary CSE operand is serialized");
1144                    values.push(format!("{prefix}:{value}"));
1145                }
1146                KeyTask::FinishBinary(prefix) => {
1147                    let right = values.pop().expect("right CSE operand is serialized");
1148                    let left = values.pop().expect("left CSE operand is serialized");
1149                    values.push(format!("{prefix}:{left}:{right}"));
1150                }
1151                KeyTask::FinishComparison(op) => {
1152                    let right = values.pop().expect("right comparison is serialized");
1153                    let left = values.pop().expect("left comparison is serialized");
1154                    values.push(format!(
1155                        "cmp:{left}:{}:{right}",
1156                        Self::compare_op_cse_key(op)
1157                    ));
1158                }
1159                KeyTask::FinishList { prefix, item_count } => {
1160                    let start = values
1161                        .len()
1162                        .checked_sub(item_count)
1163                        .expect("CSE list items are serialized");
1164                    let items = values.split_off(start);
1165                    values.push(format!("{prefix}:[{}]", items.join(",")));
1166                }
1167                KeyTask::FinishCast(target) => {
1168                    let value = values.pop().expect("cast CSE operand is serialized");
1169                    values.push(format!("cast:{target:?}:{value}"));
1170                }
1171                KeyTask::FinishConditional => {
1172                    let else_value = values.pop().expect("else CSE branch is serialized");
1173                    let then_value = values.pop().expect("then CSE branch is serialized");
1174                    let condition = values.pop().expect("CSE condition is serialized");
1175                    values.push(format!("if:{condition}:{then_value}:{else_value}"));
1176                }
1177            }
1178        }
1179
1180        let key = values.pop().expect("expression produces one CSE key");
1181        debug_assert!(values.is_empty());
1182        key
1183    }
1184
1185    fn project_expr_cse_key(expr: &ProjectExpr) -> String {
1186        match expr {
1187            ProjectExpr::Column(idx) => format!("col:{idx}"),
1188            ProjectExpr::Computed(expr, ty) => {
1189                format!("computed:{:?}:{}", ty, Self::expr_cse_key(expr))
1190            }
1191        }
1192    }
1193
1194    fn const_cse_key(value: &xlog_ir::ConstValue) -> String {
1195        match value {
1196            xlog_ir::ConstValue::U32(value) => format!("u32:{value}"),
1197            xlog_ir::ConstValue::U64(value) => format!("u64:{value}"),
1198            xlog_ir::ConstValue::I32(value) => format!("i32:{value}"),
1199            xlog_ir::ConstValue::I64(value) => format!("i64:{value}"),
1200            xlog_ir::ConstValue::F32(value) => format!("f32:{:08x}", value.to_bits()),
1201            xlog_ir::ConstValue::F64(value) => format!("f64:{:016x}", value.to_bits()),
1202            xlog_ir::ConstValue::Bool(value) => format!("bool:{value}"),
1203            xlog_ir::ConstValue::Symbol(value) => format!("symbol:{value:?}"),
1204        }
1205    }
1206
1207    fn compare_op_cse_key(op: xlog_ir::CompareOp) -> &'static str {
1208        match op {
1209            xlog_ir::CompareOp::Eq => "eq",
1210            xlog_ir::CompareOp::Ne => "ne",
1211            xlog_ir::CompareOp::Lt => "lt",
1212            xlog_ir::CompareOp::Le => "le",
1213            xlog_ir::CompareOp::Gt => "gt",
1214            xlog_ir::CompareOp::Ge => "ge",
1215        }
1216    }
1217
1218    fn store_put(&mut self, name: &str, buffer: CudaBuffer) {
1219        self.common_subexpression_cache.clear();
1220        self.store.put(name, buffer);
1221        if let Some(&rel_id) = self.name_to_rel.get(name) {
1222            self.join_index_cache.invalidate_rel(rel_id);
1223        }
1224    }
1225
1226    fn store_remove(&mut self, name: &str) -> Option<CudaBuffer> {
1227        self.common_subexpression_cache.clear();
1228        if let Some(&rel_id) = self.name_to_rel.get(name) {
1229            self.join_index_cache.invalidate_rel(rel_id);
1230        }
1231        self.store.remove(name)
1232    }
1233
1234    /// Register a relation name for a RelId
1235    ///
1236    /// This mapping is used when executing Scan nodes to look up relations
1237    /// by their RelId.
1238    ///
1239    /// # Arguments
1240    /// * `rel_id` - The relation identifier
1241    /// * `name` - The name to associate with the relation
1242    pub fn register_relation(&mut self, rel_id: RelId, name: &str) {
1243        self.rel_names.insert(rel_id, name.to_string());
1244        self.name_to_rel.insert(name.to_string(), rel_id);
1245        self.stats.register_relation(rel_id);
1246    }
1247
1248    /// Reverse-lookup a RelId by predicate name. Used by
1249    /// `execute_recursive_scc` to resolve a recursive predicate's
1250    /// full-rel RelId for `StatsManager::update_cardinality` calls at
1251    /// iteration boundaries. Returns `None` for unregistered names
1252    /// (defensive: production callers register IDB heads before
1253    /// `execute_plan`; tests that omit registration get a no-op stats
1254    /// update).
1255    fn name_to_rel_id(&self, name: &str) -> Option<RelId> {
1256        self.name_to_rel.get(name).copied()
1257    }
1258
1259    /// Get the relation name for a RelId
1260    fn get_rel_name(&self, rel_id: RelId) -> Option<&str> {
1261        self.rel_names.get(&rel_id).map(|s| s.as_str())
1262    }
1263
1264    /// Execute a baseline plan and conditionally adopt a compiler-supplied
1265    /// re-optimized candidate plan.
1266    ///
1267    /// The baseline runs through the normal [`Self::execute_plan`] path first,
1268    /// producing runtime join telemetry. If deterministic mis-plan thresholds
1269    /// fire and adaptive re-optimization is enabled, the candidate also runs
1270    /// through [`Self::execute_plan`]. Candidate outputs are compared on the GPU
1271    /// with deterministic full-row set difference; divergent or failing
1272    /// candidates roll back to the baseline relation/statistics snapshot.
1273    pub fn execute_plan_with_adaptive_candidate(
1274        &mut self,
1275        baseline_plan: &ExecutionPlan,
1276        candidate_plan: &ExecutionPlan,
1277    ) -> Result<CudaBuffer> {
1278        self.adaptive_reoptimization_stats.invocations = self
1279            .adaptive_reoptimization_stats
1280            .invocations
1281            .saturating_add(1);
1282        self.adaptive_reoptimization_stats.diagnostics.clear();
1283        let before_dtoh_calls = self.provider.host_transfer_stats().dtoh_calls;
1284
1285        self.execute_plan(baseline_plan)?;
1286        let baseline_observations = self.adaptive_join_observations.clone();
1287        self.adaptive_reoptimization_stats.last_observations = baseline_observations.clone();
1288        let decision = self.adaptive_reoptimization_decision(&baseline_observations);
1289        self.adaptive_reoptimization_stats.last_decision = Some(decision.clone());
1290
1291        match decision.action {
1292            AdaptiveReoptimizationAction::Disabled => {
1293                self.adaptive_reoptimization_stats.disabled = self
1294                    .adaptive_reoptimization_stats
1295                    .disabled
1296                    .saturating_add(1);
1297                self.record_adaptive_dtoh_delta(before_dtoh_calls);
1298                return self.clone_final_plan_output(baseline_plan);
1299            }
1300            AdaptiveReoptimizationAction::Skipped => {
1301                self.adaptive_reoptimization_stats.skipped =
1302                    self.adaptive_reoptimization_stats.skipped.saturating_add(1);
1303                self.record_adaptive_dtoh_delta(before_dtoh_calls);
1304                return self.clone_final_plan_output(baseline_plan);
1305            }
1306            AdaptiveReoptimizationAction::AttemptCandidate => {}
1307            AdaptiveReoptimizationAction::Adopted | AdaptiveReoptimizationAction::RolledBack => {
1308                unreachable!("decision replay never returns terminal adaptive actions")
1309            }
1310        }
1311
1312        let head_names = Self::plan_head_names(baseline_plan);
1313        let baseline_snapshot = self.clone_store_snapshot()?;
1314        let baseline_stats_snapshot = self.stats_snapshot();
1315
1316        if let Err(err) = self.execute_plan(candidate_plan) {
1317            self.restore_store_snapshot(baseline_snapshot);
1318            self.restore_stats_snapshot(&baseline_stats_snapshot);
1319            self.adaptive_reoptimization_stats.rolled_back = self
1320                .adaptive_reoptimization_stats
1321                .rolled_back
1322                .saturating_add(1);
1323            self.adaptive_reoptimization_stats
1324                .diagnostics
1325                .push(AdaptiveReoptimizationDiagnostic {
1326                    kind: AdaptiveReoptimizationDiagnosticKind::CandidateExecutionFailed,
1327                    message: err.to_string(),
1328                });
1329            self.adaptive_reoptimization_stats
1330                .diagnostics
1331                .push(AdaptiveReoptimizationDiagnostic {
1332                    kind: AdaptiveReoptimizationDiagnosticKind::RollbackRestoredBaseline,
1333                    message: "baseline_snapshot_restored".to_string(),
1334                });
1335            self.adaptive_reoptimization_stats.last_observations = baseline_observations;
1336            self.adaptive_reoptimization_stats.last_decision =
1337                Some(AdaptiveReoptimizationDecision {
1338                    action: AdaptiveReoptimizationAction::RolledBack,
1339                    reason: "candidate_execution_failed".to_string(),
1340                    max_misplan_ratio: decision.max_misplan_ratio,
1341                    min_misplan_ratio: decision.min_misplan_ratio,
1342                });
1343            self.record_adaptive_dtoh_delta(before_dtoh_calls);
1344            return self.clone_final_plan_output(baseline_plan);
1345        }
1346
1347        if !self.plan_outputs_match(&head_names, &baseline_snapshot)? {
1348            self.restore_store_snapshot(baseline_snapshot);
1349            self.restore_stats_snapshot(&baseline_stats_snapshot);
1350            self.adaptive_reoptimization_stats.rolled_back = self
1351                .adaptive_reoptimization_stats
1352                .rolled_back
1353                .saturating_add(1);
1354            self.adaptive_reoptimization_stats
1355                .diagnostics
1356                .push(AdaptiveReoptimizationDiagnostic {
1357                    kind: AdaptiveReoptimizationDiagnosticKind::CandidateOutputMismatch,
1358                    message: "candidate_output_mismatch".to_string(),
1359                });
1360            self.adaptive_reoptimization_stats
1361                .diagnostics
1362                .push(AdaptiveReoptimizationDiagnostic {
1363                    kind: AdaptiveReoptimizationDiagnosticKind::RollbackRestoredBaseline,
1364                    message: "baseline_snapshot_restored".to_string(),
1365                });
1366            self.adaptive_reoptimization_stats.last_observations = baseline_observations;
1367            self.adaptive_reoptimization_stats.last_decision =
1368                Some(AdaptiveReoptimizationDecision {
1369                    action: AdaptiveReoptimizationAction::RolledBack,
1370                    reason: "candidate_output_mismatch".to_string(),
1371                    max_misplan_ratio: decision.max_misplan_ratio,
1372                    min_misplan_ratio: decision.min_misplan_ratio,
1373                });
1374            self.record_adaptive_dtoh_delta(before_dtoh_calls);
1375            return self.clone_final_plan_output(baseline_plan);
1376        }
1377
1378        self.adaptive_reoptimization_stats.adopted =
1379            self.adaptive_reoptimization_stats.adopted.saturating_add(1);
1380        self.adaptive_reoptimization_stats.last_observations = baseline_observations;
1381        self.adaptive_reoptimization_stats.last_decision = Some(AdaptiveReoptimizationDecision {
1382            action: AdaptiveReoptimizationAction::Adopted,
1383            reason: "candidate_adopted".to_string(),
1384            max_misplan_ratio: decision.max_misplan_ratio,
1385            min_misplan_ratio: decision.min_misplan_ratio,
1386        });
1387        self.record_adaptive_dtoh_delta(before_dtoh_calls);
1388        self.clone_final_plan_output(candidate_plan)
1389    }
1390
1391    /// Execute a complete execution plan
1392    ///
1393    /// Iterates through strata in order, executing each one.
1394    /// Returns the result of the final query if present, or an empty buffer.
1395    ///
1396    /// # Arguments
1397    /// * `plan` - The execution plan to execute
1398    ///
1399    /// # Returns
1400    /// The result buffer from executing the plan
1401    ///
1402    /// # Errors
1403    /// Returns an error if any stratum or query execution fails
1404    pub fn execute_plan(&mut self, plan: &ExecutionPlan) -> Result<CudaBuffer> {
1405        self.adaptive_join_observations.clear();
1406        self.common_subexpression_cache.clear();
1407        // Opt-in deterministic-Datalog D2H gate. Enabled only for the
1408        // duration of this call; the provider is shared so we restore the
1409        // prior state on every exit path (including errors). This PR ships
1410        // the gate as opt-in only — known violating relational paths
1411        // (set difference, binary-join count/materialize) are scheduled for
1412        // replacement before the default flips.
1413        let gate = self.config.strict_deterministic_d2h;
1414        let prev_gate = self.provider.strict_deterministic_d2h_enabled();
1415        if gate && !prev_gate {
1416            // Only reset the violation counter when *this* call is what
1417            // engages the gate. If a caller has manually enabled the
1418            // gate to accumulate violations across a broader strict
1419            // section, we must not clobber their telemetry.
1420            self.provider.reset_deterministic_d2h_violations();
1421            self.provider.enable_strict_deterministic_d2h();
1422        }
1423        // Cloning the Arc keeps the guard independent of `self`, so the
1424        // guard can coexist with `&mut self` calls inside the strata loop.
1425        let _gate_guard = D2hGateGuard {
1426            provider: Arc::clone(&self.provider),
1427            engaged: gate,
1428            previous: prev_gate,
1429        };
1430
1431        // Execute strata in order
1432        for (idx, stratum) in plan.strata.iter().enumerate() {
1433            // Count rules and check if recursive
1434            let (num_rules, is_recursive) = stratum
1435                .sccs
1436                .iter()
1437                .map(|&scc_id| {
1438                    let rules = plan
1439                        .rules_by_scc
1440                        .get(scc_id as usize)
1441                        .map(|r| r.len())
1442                        .unwrap_or(0);
1443                    let recursive = plan
1444                        .sccs
1445                        .get(scc_id as usize)
1446                        .map(|s| s.is_recursive)
1447                        .unwrap_or(false);
1448                    (rules, recursive)
1449                })
1450                .fold((0, false), |(r, rec), (nr, nrec)| (r + nr, rec || nrec));
1451
1452            self.profiler.begin_stratum(idx, num_rules, is_recursive);
1453            self.execute_stratum_impl(stratum, plan)?;
1454
1455            // Record peak memory after stratum
1456            let mem_bytes = self.provider.memory().allocated_bytes();
1457            self.profiler.record_peak_memory(mem_bytes);
1458
1459            self.profiler.end_stratum();
1460        }
1461
1462        // Ensure all GPU work completes before returning control to callers.
1463        self.provider.device().synchronize()?;
1464        self.adaptive_reoptimization_stats.last_observations =
1465            self.adaptive_join_observations.clone();
1466
1467        // If there are no strata, return empty buffer
1468        self.provider.create_empty_buffer(Schema::new(vec![]))
1469    }
1470
1471    /// Execute a stratum (public API)
1472    ///
1473    /// This method cannot be called directly because stratum execution requires
1474    /// access to the full ExecutionPlan (for rules_by_scc mapping). Use
1475    /// `execute_plan` instead, which processes all strata with proper context.
1476    ///
1477    /// # Arguments
1478    /// * `_stratum` - The stratum (unused - see error)
1479    ///
1480    /// # Returns
1481    /// Evaluate a predicate expression for a single row
1482    #[cfg(test)]
1483    fn evaluate_predicate(
1484        expr: &Expr,
1485        columns: &[Vec<u8>],
1486        row_idx: usize,
1487        schema: &Schema,
1488    ) -> Result<bool> {
1489        match expr {
1490            Expr::Column(col_idx) => {
1491                // Interpret column value as boolean
1492                let col_type = schema.column_type(*col_idx);
1493                if let Some(ScalarType::Bool) = col_type {
1494                    Ok(columns
1495                        .get(*col_idx)
1496                        .map(|c| c.get(row_idx).copied().unwrap_or(0) != 0)
1497                        .unwrap_or(false))
1498                } else {
1499                    // Non-bool columns: check if non-zero
1500                    Ok(true)
1501                }
1502            }
1503
1504            Expr::Const(ConstValue::Bool(b)) => Ok(*b),
1505            Expr::Const(_) => Ok(true), // Non-bool constants are truthy
1506
1507            Expr::Compare { left, op, right } => {
1508                let use_float =
1509                    Self::expr_may_be_float(left, schema) || Self::expr_may_be_float(right, schema);
1510
1511                if use_float {
1512                    let left_val = Self::evaluate_expr_as_f64(left, columns, row_idx, schema)?;
1513                    let right_val = Self::evaluate_expr_as_f64(right, columns, row_idx, schema)?;
1514
1515                    Ok(match op {
1516                        CompareOp::Eq => left_val == right_val,
1517                        CompareOp::Ne => left_val != right_val,
1518                        CompareOp::Lt => left_val < right_val,
1519                        CompareOp::Le => left_val <= right_val,
1520                        CompareOp::Gt => left_val > right_val,
1521                        CompareOp::Ge => left_val >= right_val,
1522                    })
1523                } else {
1524                    let left_val = Self::evaluate_expr_as_i64(left, columns, row_idx, schema)?;
1525                    let right_val = Self::evaluate_expr_as_i64(right, columns, row_idx, schema)?;
1526
1527                    Ok(match op {
1528                        CompareOp::Eq => left_val == right_val,
1529                        CompareOp::Ne => left_val != right_val,
1530                        CompareOp::Lt => left_val < right_val,
1531                        CompareOp::Le => left_val <= right_val,
1532                        CompareOp::Gt => left_val > right_val,
1533                        CompareOp::Ge => left_val >= right_val,
1534                    })
1535                }
1536            }
1537
1538            Expr::And(exprs) => {
1539                for e in exprs {
1540                    if !Self::evaluate_predicate(e, columns, row_idx, schema)? {
1541                        return Ok(false);
1542                    }
1543                }
1544                Ok(true)
1545            }
1546
1547            Expr::Or(exprs) => {
1548                for e in exprs {
1549                    if Self::evaluate_predicate(e, columns, row_idx, schema)? {
1550                        return Ok(true);
1551                    }
1552                }
1553                Ok(false)
1554            }
1555
1556            Expr::Not(inner) => Ok(!Self::evaluate_predicate(inner, columns, row_idx, schema)?),
1557
1558            // Arithmetic expressions are not used as predicates directly
1559            Expr::Add(_, _)
1560            | Expr::Sub(_, _)
1561            | Expr::Mul(_, _)
1562            | Expr::Div(_, _)
1563            | Expr::Mod(_, _)
1564            | Expr::Abs(_)
1565            | Expr::Min(_, _)
1566            | Expr::Max(_, _)
1567            | Expr::Pow(_, _)
1568            | Expr::Cast(_, _)
1569            | Expr::Conditional { .. } => Err(XlogError::Execution(
1570                "Arithmetic expression cannot be evaluated as boolean predicate".into(),
1571            )),
1572        }
1573    }
1574
1575    #[cfg(test)]
1576    fn evaluate_expr_as_f64(
1577        expr: &Expr,
1578        columns: &[Vec<u8>],
1579        row_idx: usize,
1580        schema: &Schema,
1581    ) -> Result<f64> {
1582        match expr {
1583            Expr::Column(col_idx) => {
1584                let col_type = schema.column_type(*col_idx).unwrap_or(ScalarType::U32);
1585                let col_data = columns
1586                    .get(*col_idx)
1587                    .ok_or_else(|| XlogError::Execution(format!("Column {} not found", col_idx)))?;
1588
1589                let type_size = col_type.size_bytes();
1590                let start = row_idx * type_size;
1591
1592                Ok(match col_type {
1593                    ScalarType::F64 => {
1594                        let bytes = &col_data[start..start + 8];
1595                        f64::from_le_bytes([
1596                            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1597                            bytes[7],
1598                        ])
1599                    }
1600                    ScalarType::F32 => {
1601                        let bytes = &col_data[start..start + 4];
1602                        f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64
1603                    }
1604                    ScalarType::U32 | ScalarType::Symbol => {
1605                        let bytes = &col_data[start..start + 4];
1606                        u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64
1607                    }
1608                    ScalarType::I32 => {
1609                        let bytes = &col_data[start..start + 4];
1610                        i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64
1611                    }
1612                    ScalarType::U64 => {
1613                        let bytes = &col_data[start..start + 8];
1614                        u64::from_le_bytes([
1615                            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1616                            bytes[7],
1617                        ]) as f64
1618                    }
1619                    ScalarType::I64 => {
1620                        let bytes = &col_data[start..start + 8];
1621                        i64::from_le_bytes([
1622                            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1623                            bytes[7],
1624                        ]) as f64
1625                    }
1626                    ScalarType::Bool => col_data.get(start).copied().unwrap_or(0) as f64,
1627                })
1628            }
1629
1630            Expr::Const(val) => Ok(match val {
1631                ConstValue::U32(v) => *v as f64,
1632                ConstValue::I32(v) => *v as f64,
1633                ConstValue::U64(v) => *v as f64,
1634                ConstValue::I64(v) => *v as f64,
1635                ConstValue::Bool(b) => {
1636                    if *b {
1637                        1.0
1638                    } else {
1639                        0.0
1640                    }
1641                }
1642                ConstValue::F32(f) => *f as f64,
1643                ConstValue::F64(f) => *f,
1644                ConstValue::Symbol(_) => {
1645                    return Err(XlogError::Execution(
1646                        "Cannot evaluate Symbol constant as f64".to_string(),
1647                    ));
1648                }
1649            }),
1650
1651            Expr::Add(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1652                + Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?),
1653            Expr::Sub(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1654                - Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?),
1655            Expr::Mul(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1656                * Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?),
1657            Expr::Div(l, r) => {
1658                let left_val = Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?;
1659                let right_val = Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?;
1660                if right_val == 0.0 {
1661                    return Err(XlogError::Execution("Division by zero".to_string()));
1662                }
1663                Ok(left_val / right_val)
1664            }
1665            Expr::Mod(l, r) => {
1666                let left_val = Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?;
1667                let right_val = Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?;
1668                if right_val == 0.0 {
1669                    return Err(XlogError::Execution("Modulo by zero".to_string()));
1670                }
1671                Ok(left_val % right_val)
1672            }
1673            Expr::Abs(inner) => {
1674                Ok(Self::evaluate_expr_as_f64(inner, columns, row_idx, schema)?.abs())
1675            }
1676            Expr::Min(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1677                .min(Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?)),
1678            Expr::Max(l, r) => Ok(Self::evaluate_expr_as_f64(l, columns, row_idx, schema)?
1679                .max(Self::evaluate_expr_as_f64(r, columns, row_idx, schema)?)),
1680            Expr::Pow(base, exp) => Ok(Self::evaluate_expr_as_f64(base, columns, row_idx, schema)?
1681                .powf(Self::evaluate_expr_as_f64(exp, columns, row_idx, schema)?)),
1682            Expr::Cast(inner, target_type) => match target_type {
1683                ScalarType::F64 => Self::evaluate_expr_as_f64(inner, columns, row_idx, schema),
1684                ScalarType::F32 => {
1685                    Ok(Self::evaluate_expr_as_f64(inner, columns, row_idx, schema)? as f32 as f64)
1686                }
1687                _ => Ok(Self::evaluate_expr_as_i64(inner, columns, row_idx, schema)? as f64),
1688            },
1689
1690            _ => Err(XlogError::Execution(
1691                "Cannot evaluate compound expression as f64".to_string(),
1692            )),
1693        }
1694    }
1695
1696    /// Evaluate an expression as an i64 value
1697    #[cfg(test)]
1698    fn evaluate_expr_as_i64(
1699        expr: &Expr,
1700        columns: &[Vec<u8>],
1701        row_idx: usize,
1702        schema: &Schema,
1703    ) -> Result<i64> {
1704        match expr {
1705            Expr::Column(col_idx) => {
1706                let col_type = schema.column_type(*col_idx).unwrap_or(ScalarType::U32);
1707                let col_data = columns
1708                    .get(*col_idx)
1709                    .ok_or_else(|| XlogError::Execution(format!("Column {} not found", col_idx)))?;
1710
1711                let type_size = col_type.size_bytes();
1712                let start = row_idx * type_size;
1713
1714                Ok(match col_type {
1715                    ScalarType::U32 => {
1716                        let bytes = &col_data[start..start + 4];
1717                        u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i64
1718                    }
1719                    ScalarType::I32 => {
1720                        let bytes = &col_data[start..start + 4];
1721                        i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i64
1722                    }
1723                    ScalarType::U64 => {
1724                        let bytes = &col_data[start..start + 8];
1725                        u64::from_le_bytes([
1726                            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1727                            bytes[7],
1728                        ]) as i64
1729                    }
1730                    ScalarType::I64 => {
1731                        let bytes = &col_data[start..start + 8];
1732                        i64::from_le_bytes([
1733                            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1734                            bytes[7],
1735                        ])
1736                    }
1737                    ScalarType::Bool => col_data.get(start).copied().unwrap_or(0) as i64,
1738                    ScalarType::Symbol => {
1739                        let bytes = &col_data[start..start + 4];
1740                        u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i64
1741                    }
1742                    ScalarType::F32 => {
1743                        let bytes = &col_data[start..start + 4];
1744                        let val = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1745                        val as i64
1746                    }
1747                    ScalarType::F64 => {
1748                        let bytes = &col_data[start..start + 8];
1749                        let val = f64::from_le_bytes([
1750                            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
1751                            bytes[7],
1752                        ]);
1753                        val as i64
1754                    }
1755                })
1756            }
1757
1758            Expr::Const(val) => Ok(match val {
1759                ConstValue::U32(v) => *v as i64,
1760                ConstValue::I32(v) => *v as i64,
1761                ConstValue::U64(v) => *v as i64,
1762                ConstValue::I64(v) => *v,
1763                ConstValue::Bool(b) => *b as i64,
1764                ConstValue::F32(f) => *f as i64,
1765                ConstValue::F64(f) => *f as i64,
1766                ConstValue::Symbol(_) => 0,
1767            }),
1768
1769            // Arithmetic expressions - evaluate them and return the result
1770            Expr::Add(l, r) => {
1771                let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1772                let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1773                Ok(left_val.wrapping_add(right_val))
1774            }
1775            Expr::Sub(l, r) => {
1776                let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1777                let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1778                Ok(left_val.wrapping_sub(right_val))
1779            }
1780            Expr::Mul(l, r) => {
1781                let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1782                let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1783                Ok(left_val.wrapping_mul(right_val))
1784            }
1785            Expr::Div(l, r) => {
1786                let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1787                let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1788                if right_val == 0 {
1789                    return Err(XlogError::Execution("Division by zero".to_string()));
1790                }
1791                Ok(left_val / right_val)
1792            }
1793            Expr::Mod(l, r) => {
1794                let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1795                let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1796                if right_val == 0 {
1797                    return Err(XlogError::Execution("Modulo by zero".to_string()));
1798                }
1799                Ok(left_val % right_val)
1800            }
1801            Expr::Abs(inner) => {
1802                let val = Self::evaluate_expr_as_i64(inner, columns, row_idx, schema)?;
1803                Ok(val.abs())
1804            }
1805            Expr::Min(l, r) => {
1806                let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1807                let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1808                Ok(left_val.min(right_val))
1809            }
1810            Expr::Max(l, r) => {
1811                let left_val = Self::evaluate_expr_as_i64(l, columns, row_idx, schema)?;
1812                let right_val = Self::evaluate_expr_as_i64(r, columns, row_idx, schema)?;
1813                Ok(left_val.max(right_val))
1814            }
1815            Expr::Pow(base, exp) => {
1816                let base_val = Self::evaluate_expr_as_i64(base, columns, row_idx, schema)?;
1817                let exp_val = Self::evaluate_expr_as_i64(exp, columns, row_idx, schema)?;
1818                if exp_val < 0 {
1819                    Err(XlogError::Execution(
1820                        "Negative exponent in integer pow".to_string(),
1821                    ))
1822                } else if exp_val > u32::MAX as i64 {
1823                    // Exponent too large - would overflow anyway
1824                    Ok(i64::MAX)
1825                } else {
1826                    Ok(base_val.pow(exp_val as u32))
1827                }
1828            }
1829            Expr::Cast(inner, _target_type) => {
1830                // For i64 evaluation, cast is a no-op since we evaluate everything as i64
1831                Self::evaluate_expr_as_i64(inner, columns, row_idx, schema)
1832            }
1833
1834            _ => Err(XlogError::Execution(
1835                "Cannot evaluate compound expression as value".to_string(),
1836            )),
1837        }
1838    }
1839
1840    /// Get the relation name for a RelId, creating a default name if not registered
1841    fn get_or_create_rel_name(&mut self, rel_id: RelId, default: &str) -> String {
1842        if let Some(name) = self.rel_names.get(&rel_id) {
1843            name.clone()
1844        } else {
1845            self.register_relation(rel_id, default);
1846            default.to_string()
1847        }
1848    }
1849
1850    // ============== Helper methods ==============
1851
1852    /// Create an empty buffer with the given schema
1853    fn create_empty_buffer(&self, schema: Schema) -> Result<CudaBuffer> {
1854        self.provider.create_empty_buffer(schema)
1855    }
1856
1857    /// Clone a buffer (device-to-device copy)
1858    fn clone_buffer(&self, buffer: &CudaBuffer) -> Result<CudaBuffer> {
1859        if buffer.is_empty() {
1860            return self.create_empty_buffer(buffer.schema().clone());
1861        }
1862
1863        let mut result_columns = Vec::with_capacity(buffer.arity());
1864
1865        for col_idx in 0..buffer.arity() {
1866            let col_type_size = buffer
1867                .schema()
1868                .column_type(col_idx)
1869                .map(|t| t.size_bytes())
1870                .unwrap_or(4);
1871            let bytes = (buffer.num_rows() as usize) * col_type_size;
1872
1873            if let Some(src_col) = buffer.column(col_idx) {
1874                let mut dst_col = self.provider.memory().alloc::<u8>(bytes)?;
1875                if bytes > 0 {
1876                    self.provider
1877                        .device()
1878                        .inner()
1879                        .dtod_copy(src_col, &mut dst_col)
1880                        .map_err(|e| {
1881                            XlogError::Execution(format!("Failed to clone column on device: {}", e))
1882                        })?;
1883                }
1884                result_columns.push(dst_col.into());
1885            }
1886        }
1887
1888        let d_num_rows = self.clone_device_row_count(buffer)?;
1889        let cloned = match buffer.cached_row_count() {
1890            Some(row_count) => CudaBuffer::from_columns_with_host_count(
1891                result_columns,
1892                buffer.num_rows(),
1893                d_num_rows,
1894                buffer.schema().clone(),
1895                row_count,
1896            ),
1897            None => CudaBuffer::from_columns(
1898                result_columns,
1899                buffer.num_rows(),
1900                d_num_rows,
1901                buffer.schema().clone(),
1902            ),
1903        };
1904        Ok(cloned)
1905    }
1906
1907    fn clone_device_row_count(&self, buffer: &CudaBuffer) -> Result<TrackedCudaSlice<u32>> {
1908        let mut d_num_rows = self.provider.memory().alloc::<u32>(1)?;
1909        self.provider
1910            .device()
1911            .inner()
1912            .dtod_copy(buffer.num_rows_device(), &mut d_num_rows)
1913            .map_err(|e| XlogError::Execution(format!("Failed to copy row count: {}", e)))?;
1914        Ok(d_num_rows)
1915    }
1916
1917    fn buffer_row_count(&self, buffer: &CudaBuffer) -> Result<u32> {
1918        // Metadata-only read: row counts are control-plane state, not
1919        // tuple data. Route through the provider's audited helper so the
1920        // metadata-vs-data-plane contract stays grepable and the
1921        // deterministic-D2H gate continues to allow it. Re-map the
1922        // provider-level `XlogError::Kernel` into `XlogError::Execution`
1923        // with the executor's historical "Failed to read row count"
1924        // context so callers see a consistent error category.
1925        let n = self
1926            .provider
1927            .device_row_count(buffer)
1928            .map_err(|e| XlogError::Execution(format!("Failed to read row count: {}", e)))?;
1929        u32::try_from(n).map_err(|_| {
1930            XlogError::Execution(format!("Row count {n} exceeds the supported u32 range"))
1931        })
1932    }
1933}
1934
1935/// RAII guard that restores the provider's deterministic-D2H gate state on
1936/// drop. Engaged only when `Executor::execute_plan` opted in via
1937/// `RuntimeConfig::strict_deterministic_d2h`.
1938struct D2hGateGuard {
1939    provider: Arc<CudaKernelProvider>,
1940    engaged: bool,
1941    previous: bool,
1942}
1943
1944impl Drop for D2hGateGuard {
1945    fn drop(&mut self) {
1946        if !self.engaged {
1947            return;
1948        }
1949        if self.previous {
1950            self.provider.enable_strict_deterministic_d2h();
1951        } else {
1952            self.provider.disable_strict_deterministic_d2h();
1953        }
1954    }
1955}
1956
1957#[cfg(test)]
1958mod tests {
1959    use super::*;
1960    use std::time::{Duration, Instant};
1961    use xlog_core::MemoryBudget;
1962    use xlog_cuda::{CudaDevice, GpuMemoryManager};
1963    use xlog_ir::{CompiledRule, RirMeta, Scc};
1964
1965    fn has_cuda_device() -> bool {
1966        // Check if CUDA device is available using CudaDevice wrapper
1967        CudaDevice::new(0).is_ok()
1968    }
1969
1970    fn create_test_executor() -> Option<Executor> {
1971        if !has_cuda_device() {
1972            return None;
1973        }
1974        let device = Arc::new(CudaDevice::new(0).ok()?);
1975        let budget = MemoryBudget::with_limit(1024 * 1024 * 1024); // 1 GB
1976        let memory = Arc::new(GpuMemoryManager::new(device.clone(), budget));
1977        let provider = Arc::new(CudaKernelProvider::new(device, memory).ok()?);
1978        Some(Executor::new(provider))
1979    }
1980
1981    fn create_test_executor_with_config(config: RuntimeConfig) -> Option<Executor> {
1982        if !has_cuda_device() {
1983            return None;
1984        }
1985        let device = Arc::new(CudaDevice::new(0).ok()?);
1986        let budget = MemoryBudget::with_limit(1024 * 1024 * 1024); // 1 GB
1987        let memory = Arc::new(GpuMemoryManager::new(device.clone(), budget));
1988        let provider = Arc::new(CudaKernelProvider::new(device, memory).ok()?);
1989        Some(Executor::new_with_config(provider, config))
1990    }
1991
1992    fn create_manager_stats_fixture() -> Option<(Arc<GpuMemoryManager>, Arc<CudaKernelProvider>)> {
1993        let device = match CudaDevice::new(0) {
1994            Ok(device) => Arc::new(device),
1995            Err(error) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
1996                panic!("XLOG_REQUIRE_CUDA=1 but CUDA initialization failed: {error}")
1997            }
1998            Err(error) => {
1999                eprintln!("Skipping test: CUDA runtime unavailable: {error}");
2000                return None;
2001            }
2002        };
2003        let memory = Arc::new(GpuMemoryManager::new(
2004            Arc::clone(&device),
2005            MemoryBudget::with_limit(1024 * 1024 * 1024),
2006        ));
2007        let provider = match CudaKernelProvider::new(device, Arc::clone(&memory)) {
2008            Ok(provider) => Arc::new(provider),
2009            Err(error) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
2010                panic!("XLOG_REQUIRE_CUDA=1 but provider initialization failed: {error}")
2011            }
2012            Err(error) => {
2013                eprintln!("Skipping test: provider initialization failed: {error}");
2014                return None;
2015            }
2016        };
2017        Some((memory, provider))
2018    }
2019
2020    #[test]
2021    fn manager_reservation_peak_transient_drop_before_stats() {
2022        let Some((memory, provider)) = create_manager_stats_fixture() else {
2023            return;
2024        };
2025        let mut executor = Executor::new(provider);
2026        executor.set_profiling(true);
2027        let baseline = memory.allocated_bytes();
2028        let transient = memory.alloc::<u8>(4096).expect("transient allocation");
2029        let expected_peak = baseline + 4096;
2030        drop(transient);
2031        assert_eq!(memory.allocated_bytes(), baseline);
2032
2033        assert_eq!(
2034            executor.execution_stats(0).peak_memory_bytes,
2035            expected_peak,
2036            "stats must retain a transient reservation dropped before sampling"
2037        );
2038    }
2039
2040    #[test]
2041    fn manager_reservation_peak_enabled_no_op_preserves_provider_history() {
2042        let Some((memory, provider)) = create_manager_stats_fixture() else {
2043            return;
2044        };
2045        let baseline = memory.allocated_bytes();
2046        let transient = memory.alloc::<u8>(2048).expect("historical allocation");
2047        let expected_peak = baseline + 2048;
2048        drop(transient);
2049
2050        let mut executor = Executor::new(provider);
2051        executor.set_profiling(true);
2052        assert_eq!(
2053            executor.execution_stats(0).peak_memory_bytes,
2054            expected_peak,
2055            "an enabled executor with no operations must expose provider history"
2056        );
2057    }
2058
2059    #[test]
2060    fn manager_reservation_peak_disabled_stats_still_report_manager_peak() {
2061        let Some((memory, provider)) = create_manager_stats_fixture() else {
2062            return;
2063        };
2064        let baseline = memory.allocated_bytes();
2065        let transient = memory.alloc::<u8>(3072).expect("transient allocation");
2066        let expected_peak = baseline + 3072;
2067        drop(transient);
2068
2069        let executor = Executor::new(provider);
2070        assert!(!executor.is_profiling());
2071        assert_eq!(
2072            executor.execution_stats(0).peak_memory_bytes,
2073            expected_peak,
2074            "manager peak is available independently of profiler sampling"
2075        );
2076    }
2077
2078    #[test]
2079    fn manager_reservation_peak_shared_manager_has_provider_lifetime_scope() {
2080        let Some((memory, provider)) = create_manager_stats_fixture() else {
2081            return;
2082        };
2083        let mut first = Executor::new(Arc::clone(&provider));
2084        first.set_profiling(true);
2085        let baseline = memory.allocated_bytes();
2086        let transient = memory
2087            .alloc::<u8>(5120)
2088            .expect("shared transient allocation");
2089        let expected_peak = baseline + 5120;
2090        drop(transient);
2091        assert_eq!(first.execution_stats(0).peak_memory_bytes, expected_peak);
2092
2093        let mut second = Executor::new(provider);
2094        second.set_profiling(true);
2095        assert_eq!(
2096            second.execution_stats(0).peak_memory_bytes,
2097            expected_peak,
2098            "constructing another executor must not reset the shared manager"
2099        );
2100    }
2101
2102    #[test]
2103    fn manager_reservation_peak_quiescent_reset_starts_new_window() {
2104        let Some((memory, provider)) = create_manager_stats_fixture() else {
2105            return;
2106        };
2107        let old = memory.alloc::<u8>(4096).expect("old-window allocation");
2108        drop(old);
2109        assert_eq!(memory.allocated_bytes(), 0, "reset requires quiescence");
2110        memory.reset_peak();
2111        assert_eq!(memory.peak_bytes(), 0);
2112
2113        let new = memory.alloc::<u8>(1024).expect("new-window allocation");
2114        drop(new);
2115        let executor = Executor::new(provider);
2116        assert_eq!(
2117            executor.execution_stats(0).peak_memory_bytes,
2118            1024,
2119            "stats must reflect the explicit reset window"
2120        );
2121    }
2122
2123    fn device_row_count(executor: &Executor, rows: u64) -> TrackedCudaSlice<u32> {
2124        let rows_u32 = u32::try_from(rows).expect("row count fits u32");
2125        let mut d_num_rows = executor.provider.memory().alloc::<u32>(1).expect("alloc");
2126        executor
2127            .provider
2128            .device()
2129            .inner()
2130            .htod_sync_copy_into(&[rows_u32], &mut d_num_rows)
2131            .expect("htod");
2132        d_num_rows
2133    }
2134
2135    fn create_test_buffer(executor: &Executor, data: &[u32], col_name: &str) -> CudaBuffer {
2136        let schema = Schema::new(vec![(col_name.to_string(), ScalarType::U32)]);
2137        let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_le_bytes()).collect();
2138
2139        let mut col = executor
2140            .provider
2141            .memory()
2142            .alloc::<u8>(bytes.len())
2143            .expect("alloc");
2144        executor
2145            .provider
2146            .device()
2147            .inner()
2148            .htod_sync_copy_into(&bytes, &mut col)
2149            .expect("htod");
2150
2151        let rows = data.len() as u64;
2152        let d_num_rows = device_row_count(executor, rows);
2153        CudaBuffer::from_columns(vec![col.into()], rows, d_num_rows, schema)
2154    }
2155
2156    #[test]
2157    fn clone_buffer_preserves_cached_logical_rows_without_host_transfer() {
2158        let Some((_memory, provider)) = create_manager_stats_fixture() else {
2159            return;
2160        };
2161        let executor = Executor::new(provider);
2162        let schema = Schema::new(vec![("value".to_string(), ScalarType::U32)]);
2163        let values = [10u32, 20, 99];
2164        let bytes: Vec<u8> = values
2165            .iter()
2166            .flat_map(|value| value.to_le_bytes())
2167            .collect();
2168        let mut column = executor
2169            .provider
2170            .memory()
2171            .alloc::<u8>(bytes.len())
2172            .expect("column allocation");
2173        executor
2174            .provider
2175            .device()
2176            .inner()
2177            .htod_sync_copy_into(&bytes, &mut column)
2178            .expect("column upload");
2179        let source = CudaBuffer::from_columns_with_host_count(
2180            vec![column.into()],
2181            values.len() as u64,
2182            device_row_count(&executor, 2),
2183            schema,
2184            2,
2185        );
2186
2187        executor.provider.reset_host_transfer_stats();
2188        let cloned = executor.clone_buffer(&source).expect("device clone");
2189        let clone_transfers = executor.provider.host_transfer_stats();
2190
2191        assert_eq!(cloned.cached_row_count(), Some(2));
2192        assert_eq!(clone_transfers.dtoh_calls, 0);
2193        assert_eq!(read_buffer_u32(&executor, &cloned, 0), vec![10, 20]);
2194
2195        let uncached = create_test_buffer(&executor, &[30, 40], "value");
2196        assert_eq!(uncached.cached_row_count(), None);
2197        executor.provider.reset_host_transfer_stats();
2198        let uncached_clone = executor
2199            .clone_buffer(&uncached)
2200            .expect("uncached device clone");
2201        assert_eq!(uncached_clone.cached_row_count(), None);
2202        assert_eq!(executor.provider.host_transfer_stats().dtoh_calls, 0);
2203    }
2204
2205    fn read_buffer_u32(executor: &Executor, buffer: &CudaBuffer, col: usize) -> Vec<u32> {
2206        executor
2207            .provider
2208            .download_column::<u32>(buffer, col)
2209            .unwrap_or_default()
2210    }
2211
2212    fn buffer_row_count(executor: &Executor, buffer: &CudaBuffer) -> u32 {
2213        executor
2214            .provider
2215            .device_row_count(buffer)
2216            .expect("dtoh row count") as u32
2217    }
2218
2219    fn to_f64_column_bytes(values: &[f64]) -> Vec<u8> {
2220        values.iter().flat_map(|v| v.to_le_bytes()).collect()
2221    }
2222
2223    fn to_f32_column_bytes(values: &[f32]) -> Vec<u8> {
2224        values.iter().flat_map(|v| v.to_le_bytes()).collect()
2225    }
2226
2227    // ============== Basic Executor Tests ==============
2228
2229    #[test]
2230    fn test_executor_creation() {
2231        let executor = match create_test_executor() {
2232            Some(e) => e,
2233            None => {
2234                eprintln!("Skipping test: no CUDA device available");
2235                return;
2236            }
2237        };
2238
2239        assert!(executor.store().is_empty());
2240    }
2241
2242    #[test]
2243    fn test_predicate_f64_comparisons() {
2244        let schema = Schema::new(vec![("x".to_string(), ScalarType::F64)]);
2245        let values = [1.0f64, 2.0, 3.0, f64::NAN];
2246        let columns = vec![to_f64_column_bytes(&values)];
2247
2248        let gt_two = Expr::Compare {
2249            left: Box::new(Expr::Column(0)),
2250            op: CompareOp::Gt,
2251            right: Box::new(Expr::Const(ConstValue::F64(2.0))),
2252        };
2253
2254        let results: Vec<bool> = (0..values.len())
2255            .map(|row| Executor::evaluate_predicate(&gt_two, &columns, row, &schema).unwrap())
2256            .collect();
2257        assert_eq!(results, vec![false, false, true, false]);
2258
2259        let eq_nan = Expr::Compare {
2260            left: Box::new(Expr::Column(0)),
2261            op: CompareOp::Eq,
2262            right: Box::new(Expr::Const(ConstValue::F64(f64::NAN))),
2263        };
2264        let results: Vec<bool> = (0..values.len())
2265            .map(|row| Executor::evaluate_predicate(&eq_nan, &columns, row, &schema).unwrap())
2266            .collect();
2267        assert_eq!(results, vec![false, false, false, false]);
2268
2269        let ne_nan = Expr::Compare {
2270            left: Box::new(Expr::Column(0)),
2271            op: CompareOp::Ne,
2272            right: Box::new(Expr::Const(ConstValue::F64(f64::NAN))),
2273        };
2274        let results: Vec<bool> = (0..values.len())
2275            .map(|row| Executor::evaluate_predicate(&ne_nan, &columns, row, &schema).unwrap())
2276            .collect();
2277        assert_eq!(results, vec![true, true, true, true]);
2278    }
2279
2280    #[test]
2281    fn test_predicate_f32_comparisons() {
2282        let schema = Schema::new(vec![("x".to_string(), ScalarType::F32)]);
2283        let values = [1.0f32, 2.0, 3.0, f32::NAN];
2284        let columns = vec![to_f32_column_bytes(&values)];
2285
2286        let le_two = Expr::Compare {
2287            left: Box::new(Expr::Column(0)),
2288            op: CompareOp::Le,
2289            right: Box::new(Expr::Const(ConstValue::F32(2.0))),
2290        };
2291
2292        let results: Vec<bool> = (0..values.len())
2293            .map(|row| Executor::evaluate_predicate(&le_two, &columns, row, &schema).unwrap())
2294            .collect();
2295        assert_eq!(results, vec![true, true, false, false]);
2296    }
2297
2298    #[test]
2299    fn test_predicate_mixed_float_int_comparisons() {
2300        let schema = Schema::new(vec![
2301            ("x".to_string(), ScalarType::F64),
2302            ("y".to_string(), ScalarType::U32),
2303        ]);
2304
2305        let x = [1.5f64, 2.0, 2.5];
2306        let y = [1u32, 2, 3];
2307        let columns = vec![
2308            to_f64_column_bytes(&x),
2309            y.iter().flat_map(|v| v.to_le_bytes()).collect(),
2310        ];
2311
2312        let x_gt_2 = Expr::Compare {
2313            left: Box::new(Expr::Column(0)),
2314            op: CompareOp::Gt,
2315            right: Box::new(Expr::Const(ConstValue::U32(2))),
2316        };
2317        let results: Vec<bool> = (0..x.len())
2318            .map(|row| Executor::evaluate_predicate(&x_gt_2, &columns, row, &schema).unwrap())
2319            .collect();
2320        assert_eq!(results, vec![false, false, true]);
2321
2322        let y_lt_2_5 = Expr::Compare {
2323            left: Box::new(Expr::Column(1)),
2324            op: CompareOp::Lt,
2325            right: Box::new(Expr::Const(ConstValue::F64(2.5))),
2326        };
2327        let results: Vec<bool> = (0..y.len())
2328            .map(|row| Executor::evaluate_predicate(&y_lt_2_5, &columns, row, &schema).unwrap())
2329            .collect();
2330        assert_eq!(results, vec![true, true, false]);
2331    }
2332
2333    #[test]
2334    fn test_register_and_get_relation() {
2335        let mut executor = match create_test_executor() {
2336            Some(e) => e,
2337            None => {
2338                eprintln!("Skipping test: no CUDA device available");
2339                return;
2340            }
2341        };
2342
2343        // Register a relation
2344        executor.register_relation(RelId(1), "test_rel");
2345
2346        // Verify mapping
2347        assert_eq!(executor.get_rel_name(RelId(1)), Some("test_rel"));
2348        assert_eq!(executor.get_rel_name(RelId(2)), None);
2349    }
2350
2351    // ============== Scan Node Tests ==============
2352
2353    #[test]
2354    fn test_execute_scan_not_found() {
2355        let mut executor = match create_test_executor() {
2356            Some(e) => e,
2357            None => {
2358                eprintln!("Skipping test: no CUDA device available");
2359                return;
2360            }
2361        };
2362
2363        executor.register_relation(RelId(1), "missing_rel");
2364
2365        let node = RirNode::Scan { rel: RelId(1) };
2366        let result = executor.execute_node(&node);
2367
2368        assert!(result.is_err());
2369    }
2370
2371    #[test]
2372    fn test_execute_scan_success() {
2373        let mut executor = match create_test_executor() {
2374            Some(e) => e,
2375            None => {
2376                eprintln!("Skipping test: no CUDA device available");
2377                return;
2378            }
2379        };
2380
2381        // Create and store a buffer
2382        let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2383        executor.store_mut().put("test_rel", buffer);
2384        executor.register_relation(RelId(1), "test_rel");
2385
2386        // Execute scan
2387        let node = RirNode::Scan { rel: RelId(1) };
2388        let result = executor.execute_node(&node);
2389
2390        assert!(result.is_ok());
2391        let result = result.unwrap();
2392        assert_eq!(buffer_row_count(&executor, &result), 5);
2393
2394        let values = read_buffer_u32(&executor, &result, 0);
2395        assert_eq!(values, vec![1, 2, 3, 4, 5]);
2396    }
2397
2398    // ============== Filter Node Tests ==============
2399
2400    #[test]
2401    fn test_execute_filter_empty_input() {
2402        let executor = match create_test_executor() {
2403            Some(e) => e,
2404            None => {
2405                eprintln!("Skipping test: no CUDA device available");
2406                return;
2407            }
2408        };
2409
2410        let schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2411        let empty = executor.create_empty_buffer(schema).unwrap();
2412
2413        let predicate = Expr::Const(ConstValue::Bool(true));
2414        let result = executor.execute_filter(&empty, &predicate);
2415
2416        assert!(result.is_ok());
2417        let result = result.unwrap();
2418        assert_eq!(buffer_row_count(&executor, &result), 0);
2419    }
2420
2421    #[test]
2422    fn test_execute_filter_all_match() {
2423        let executor = match create_test_executor() {
2424            Some(e) => e,
2425            None => {
2426                eprintln!("Skipping test: no CUDA device available");
2427                return;
2428            }
2429        };
2430
2431        let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2432        let predicate = Expr::Const(ConstValue::Bool(true));
2433
2434        let result = executor.execute_filter(&buffer, &predicate);
2435        assert!(result.is_ok());
2436
2437        let result = result.unwrap();
2438        assert_eq!(buffer_row_count(&executor, &result), 5);
2439    }
2440
2441    #[test]
2442    fn test_execute_filter_none_match() {
2443        let executor = match create_test_executor() {
2444            Some(e) => e,
2445            None => {
2446                eprintln!("Skipping test: no CUDA device available");
2447                return;
2448            }
2449        };
2450
2451        let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2452        let predicate = Expr::Const(ConstValue::Bool(false));
2453
2454        let result = executor.execute_filter(&buffer, &predicate);
2455        assert!(result.is_ok());
2456        let result = result.unwrap();
2457        assert_eq!(buffer_row_count(&executor, &result), 0);
2458    }
2459
2460    #[test]
2461    fn test_execute_filter_comparison() {
2462        let executor = match create_test_executor() {
2463            Some(e) => e,
2464            None => {
2465                eprintln!("Skipping test: no CUDA device available");
2466                return;
2467            }
2468        };
2469
2470        let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2471
2472        // Filter: key > 3
2473        let predicate = Expr::Compare {
2474            left: Box::new(Expr::Column(0)),
2475            op: CompareOp::Gt,
2476            right: Box::new(Expr::Const(ConstValue::U32(3))),
2477        };
2478
2479        let result = executor.execute_filter(&buffer, &predicate);
2480        assert!(result.is_ok());
2481
2482        let result = result.unwrap();
2483        assert_eq!(buffer_row_count(&executor, &result), 2);
2484
2485        let values = read_buffer_u32(&executor, &result, 0);
2486        assert_eq!(values, vec![4, 5]);
2487    }
2488
2489    #[test]
2490    fn test_execute_filter_and() {
2491        let executor = match create_test_executor() {
2492            Some(e) => e,
2493            None => {
2494                eprintln!("Skipping test: no CUDA device available");
2495                return;
2496            }
2497        };
2498
2499        let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2500
2501        // Filter: key >= 2 AND key <= 4
2502        let predicate = Expr::And(vec![
2503            Expr::Compare {
2504                left: Box::new(Expr::Column(0)),
2505                op: CompareOp::Ge,
2506                right: Box::new(Expr::Const(ConstValue::U32(2))),
2507            },
2508            Expr::Compare {
2509                left: Box::new(Expr::Column(0)),
2510                op: CompareOp::Le,
2511                right: Box::new(Expr::Const(ConstValue::U32(4))),
2512            },
2513        ]);
2514
2515        let result = executor.execute_filter(&buffer, &predicate);
2516        assert!(result.is_ok());
2517
2518        let result = result.unwrap();
2519        assert_eq!(buffer_row_count(&executor, &result), 3);
2520
2521        let values = read_buffer_u32(&executor, &result, 0);
2522        assert_eq!(values, vec![2, 3, 4]);
2523    }
2524
2525    // ============== Project Node Tests ==============
2526
2527    #[test]
2528    fn test_execute_project_empty_input() {
2529        let executor = match create_test_executor() {
2530            Some(e) => e,
2531            None => {
2532                eprintln!("Skipping test: no CUDA device available");
2533                return;
2534            }
2535        };
2536
2537        let schema = Schema::new(vec![
2538            ("a".to_string(), ScalarType::U32),
2539            ("b".to_string(), ScalarType::U32),
2540        ]);
2541        let empty = executor.create_empty_buffer(schema).unwrap();
2542
2543        let result = executor.execute_project(&empty, &[ProjectExpr::Column(0)]);
2544        assert!(result.is_ok());
2545
2546        let result = result.unwrap();
2547        assert_eq!(buffer_row_count(&executor, &result), 0);
2548        assert_eq!(result.arity(), 1);
2549    }
2550
2551    #[test]
2552    fn test_execute_zero_column_project_preserves_row_existence() {
2553        let executor = match create_test_executor() {
2554            Some(e) => e,
2555            None => {
2556                eprintln!("Skipping test: no CUDA device available");
2557                return;
2558            }
2559        };
2560
2561        let input = create_test_buffer(&executor, &[1, 2, 3], "key");
2562        let result = executor
2563            .execute_project(&input, &[])
2564            .expect("zero-column projection must execute");
2565
2566        assert_eq!(result.arity(), 0);
2567        assert_eq!(buffer_row_count(&executor, &result), 3);
2568    }
2569
2570    #[test]
2571    fn test_execute_project_reorder() {
2572        let executor = match create_test_executor() {
2573            Some(e) => e,
2574            None => {
2575                eprintln!("Skipping test: no CUDA device available");
2576                return;
2577            }
2578        };
2579
2580        // Create a 2-column buffer
2581        let schema = Schema::new(vec![
2582            ("a".to_string(), ScalarType::U32),
2583            ("b".to_string(), ScalarType::U32),
2584        ]);
2585
2586        let a_data: Vec<u8> = [1u32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
2587        let b_data: Vec<u8> = [10u32, 20, 30]
2588            .iter()
2589            .flat_map(|v| v.to_le_bytes())
2590            .collect();
2591
2592        let mut col_a = executor
2593            .provider
2594            .memory()
2595            .alloc::<u8>(a_data.len())
2596            .unwrap();
2597        let mut col_b = executor
2598            .provider
2599            .memory()
2600            .alloc::<u8>(b_data.len())
2601            .unwrap();
2602
2603        executor
2604            .provider
2605            .device()
2606            .inner()
2607            .htod_sync_copy_into(&a_data, &mut col_a)
2608            .unwrap();
2609        executor
2610            .provider
2611            .device()
2612            .inner()
2613            .htod_sync_copy_into(&b_data, &mut col_b)
2614            .unwrap();
2615
2616        let d_num_rows = device_row_count(&executor, 3);
2617        let buffer =
2618            CudaBuffer::from_columns(vec![col_a.into(), col_b.into()], 3, d_num_rows, schema);
2619
2620        // Project: [b, a] (reverse order)
2621        let result =
2622            executor.execute_project(&buffer, &[ProjectExpr::Column(1), ProjectExpr::Column(0)]);
2623        assert!(result.is_ok());
2624
2625        let result = result.unwrap();
2626        assert_eq!(buffer_row_count(&executor, &result), 3);
2627        assert_eq!(result.arity(), 2);
2628
2629        // First column should be b's values
2630        let col0 = read_buffer_u32(&executor, &result, 0);
2631        assert_eq!(col0, vec![10, 20, 30]);
2632
2633        // Second column should be a's values
2634        let col1 = read_buffer_u32(&executor, &result, 1);
2635        assert_eq!(col1, vec![1, 2, 3]);
2636    }
2637
2638    #[test]
2639    fn test_execute_computed_projection_wiring() {
2640        // Test that ProjectExpr::Computed is handled correctly
2641        // Even if arithmetic stubs return errors, verify the flow is correct
2642        let executor = match create_test_executor() {
2643            Some(e) => e,
2644            None => {
2645                eprintln!("Skipping test: no CUDA device available");
2646                return;
2647            }
2648        };
2649
2650        // Create a 2-column buffer
2651        let schema = Schema::new(vec![
2652            ("a".to_string(), ScalarType::U32),
2653            ("b".to_string(), ScalarType::U32),
2654        ]);
2655
2656        let a_data: Vec<u8> = [10u32, 20, 30]
2657            .iter()
2658            .flat_map(|v| v.to_le_bytes())
2659            .collect();
2660        let b_data: Vec<u8> = [1u32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
2661
2662        let mut col_a = executor
2663            .provider
2664            .memory()
2665            .alloc::<u8>(a_data.len())
2666            .unwrap();
2667        let mut col_b = executor
2668            .provider
2669            .memory()
2670            .alloc::<u8>(b_data.len())
2671            .unwrap();
2672
2673        executor
2674            .provider
2675            .device()
2676            .inner()
2677            .htod_sync_copy_into(&a_data, &mut col_a)
2678            .unwrap();
2679        executor
2680            .provider
2681            .device()
2682            .inner()
2683            .htod_sync_copy_into(&b_data, &mut col_b)
2684            .unwrap();
2685
2686        let d_num_rows = device_row_count(&executor, 3);
2687        let buffer =
2688            CudaBuffer::from_columns(vec![col_a.into(), col_b.into()], 3, d_num_rows, schema);
2689
2690        // Project with computed expression: a + b
2691        let add_expr = Expr::Add(Box::new(Expr::Column(0)), Box::new(Expr::Column(1)));
2692        let projections = vec![
2693            ProjectExpr::Column(0),                           // Pass through column a
2694            ProjectExpr::Computed(add_expr, ScalarType::U32), // Compute a + b
2695        ];
2696
2697        let result = executor.execute_project(&buffer, &projections);
2698
2699        // The wiring should be correct - result depends on whether CUDA arithmetic kernels are available
2700        // If available: result has 2 columns with computed values
2701        // If not available: may return error from provider stubs
2702        match result {
2703            Ok(res) => {
2704                // Wiring worked and arithmetic kernels are available
2705                assert_eq!(buffer_row_count(&executor, &res), 3);
2706                assert_eq!(res.arity(), 2);
2707
2708                // First column should be a's values (pass-through)
2709                let col0 = read_buffer_u32(&executor, &res, 0);
2710                assert_eq!(col0, vec![10, 20, 30]);
2711
2712                // Second column should be a + b = [11, 22, 33]
2713                let col1 = read_buffer_u32(&executor, &res, 1);
2714                assert_eq!(col1, vec![11, 22, 33]);
2715            }
2716            Err(e) => {
2717                // Arithmetic kernels not available - that's OK for this test
2718                // The important thing is that the wiring reached the provider
2719                let err_msg = format!("{}", e);
2720                assert!(
2721                    err_msg.contains("not implemented")
2722                        || err_msg.contains("not yet implemented")
2723                        || err_msg.contains("not supported")
2724                        || err_msg.contains("stub")
2725                        || err_msg.contains("Unsupported")
2726                        || err_msg.contains("arithmetic kernels"),
2727                    "Unexpected error: {}. Expected arithmetic kernel stub error.",
2728                    err_msg
2729                );
2730            }
2731        }
2732    }
2733
2734    #[test]
2735    fn arithmetic_expression_evaluation_reports_left_operand_error_first() {
2736        let executor = match create_test_executor() {
2737            Some(executor) => executor,
2738            None => {
2739                eprintln!("Skipping test: no CUDA device available");
2740                return;
2741            }
2742        };
2743        let input = create_test_buffer(&executor, &[1], "key");
2744        let expression = Expr::Add(Box::new(Expr::Column(99)), Box::new(Expr::Column(98)));
2745
2746        let error = match executor.evaluate_arith_expr(&expression, &input) {
2747            Ok(_) => panic!("invalid left operand must fail"),
2748            Err(error) => error,
2749        };
2750
2751        assert!(error.to_string().contains("Column 99 not found"), "{error}");
2752    }
2753
2754    #[test]
2755    fn conditional_expression_evaluation_reports_condition_error_before_branches() {
2756        let executor = match create_test_executor() {
2757            Some(executor) => executor,
2758            None => {
2759                eprintln!("Skipping test: no CUDA device available");
2760                return;
2761            }
2762        };
2763        let input = create_test_buffer(&executor, &[1], "key");
2764        let expression = Expr::Conditional {
2765            condition: Box::new(Expr::Column(99)),
2766            then_expr: Box::new(Expr::Column(98)),
2767            else_expr: Box::new(Expr::Column(97)),
2768        };
2769
2770        let error = match executor.evaluate_arith_expr(&expression, &input) {
2771            Ok(_) => panic!("invalid condition must fail"),
2772            Err(error) => error,
2773        };
2774
2775        assert!(error.to_string().contains("Column 99 not found"), "{error}");
2776    }
2777
2778    #[test]
2779    fn conditional_expression_eagerly_evaluates_then_before_else() {
2780        let executor = match create_test_executor() {
2781            Some(executor) => executor,
2782            None => {
2783                eprintln!("Skipping test: no CUDA device available");
2784                return;
2785            }
2786        };
2787        let input = create_test_buffer(&executor, &[1], "key");
2788        let invalid_then = Expr::Conditional {
2789            condition: Box::new(Expr::Const(ConstValue::Bool(false))),
2790            then_expr: Box::new(Expr::Column(98)),
2791            else_expr: Box::new(Expr::Column(97)),
2792        };
2793        let error = match executor.evaluate_arith_expr(&invalid_then, &input) {
2794            Ok(_) => panic!("eager invalid then branch must fail"),
2795            Err(error) => error,
2796        };
2797        assert!(error.to_string().contains("Column 98 not found"), "{error}");
2798
2799        let invalid_else = Expr::Conditional {
2800            condition: Box::new(Expr::Const(ConstValue::Bool(true))),
2801            then_expr: Box::new(Expr::Column(0)),
2802            else_expr: Box::new(Expr::Column(97)),
2803        };
2804        let error = match executor.evaluate_arith_expr(&invalid_else, &input) {
2805            Ok(_) => panic!("eager invalid else branch must fail"),
2806            Err(error) => error,
2807        };
2808        assert!(error.to_string().contains("Column 97 not found"), "{error}");
2809    }
2810
2811    // ============== Union Node Tests ==============
2812
2813    #[test]
2814    fn test_execute_union_empty_inputs() {
2815        let executor = match create_test_executor() {
2816            Some(e) => e,
2817            None => {
2818                eprintln!("Skipping test: no CUDA device available");
2819                return;
2820            }
2821        };
2822
2823        let result = executor.execute_union(&[]);
2824        assert!(result.is_ok());
2825        let result = result.unwrap();
2826        assert_eq!(buffer_row_count(&executor, &result), 0);
2827    }
2828
2829    #[test]
2830    fn test_execute_union_single_input() {
2831        let executor = match create_test_executor() {
2832            Some(e) => e,
2833            None => {
2834                eprintln!("Skipping test: no CUDA device available");
2835                return;
2836            }
2837        };
2838
2839        let buffer = create_test_buffer(&executor, &[1, 2, 3], "key");
2840
2841        let result = executor.execute_union(&[buffer]);
2842        assert!(result.is_ok());
2843
2844        let result = result.unwrap();
2845        assert_eq!(buffer_row_count(&executor, &result), 3);
2846
2847        let values = read_buffer_u32(&executor, &result, 0);
2848        assert_eq!(values, vec![1, 2, 3]);
2849    }
2850
2851    #[test]
2852    fn test_execute_union_multiple_inputs() {
2853        let executor = match create_test_executor() {
2854            Some(e) => e,
2855            None => {
2856                eprintln!("Skipping test: no CUDA device available");
2857                return;
2858            }
2859        };
2860
2861        let buffer1 = create_test_buffer(&executor, &[1, 2], "key");
2862        let buffer2 = create_test_buffer(&executor, &[3, 4], "key");
2863        let buffer3 = create_test_buffer(&executor, &[5], "key");
2864
2865        let result = executor.execute_union(&[buffer1, buffer2, buffer3]);
2866        assert!(result.is_ok());
2867
2868        let result = result.unwrap();
2869        assert_eq!(buffer_row_count(&executor, &result), 5);
2870    }
2871
2872    // ============== Distinct Node Tests ==============
2873
2874    #[test]
2875    fn test_execute_distinct_empty() {
2876        let executor = match create_test_executor() {
2877            Some(e) => e,
2878            None => {
2879                eprintln!("Skipping test: no CUDA device available");
2880                return;
2881            }
2882        };
2883
2884        let schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2885        let empty = executor.create_empty_buffer(schema).unwrap();
2886
2887        let result = executor.execute_distinct(&empty, &[0]);
2888        assert!(result.is_ok());
2889        let result = result.unwrap();
2890        assert_eq!(buffer_row_count(&executor, &result), 0);
2891    }
2892
2893    // ============== Diff Node Tests ==============
2894
2895    #[test]
2896    fn test_execute_diff() {
2897        let executor = match create_test_executor() {
2898            Some(e) => e,
2899            None => {
2900                eprintln!("Skipping test: no CUDA device available");
2901                return;
2902            }
2903        };
2904
2905        let left = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
2906        let right = create_test_buffer(&executor, &[2, 4], "key");
2907
2908        let result = executor.execute_diff(&left, &right);
2909        assert!(result.is_ok());
2910
2911        let result = result.unwrap();
2912        assert_eq!(buffer_row_count(&executor, &result), 3);
2913
2914        let values = read_buffer_u32(&executor, &result, 0);
2915        assert_eq!(values, vec![1, 3, 5]);
2916    }
2917
2918    // ============== Fixpoint Tests ==============
2919
2920    #[test]
2921    fn test_execute_fixpoint_base_only() {
2922        // Test fixpoint with a base case that reaches fixpoint immediately
2923        // (recursive step produces nothing new)
2924        let mut executor = match create_test_executor() {
2925            Some(e) => e,
2926            None => {
2927                eprintln!("Skipping test: no CUDA device available");
2928                return;
2929            }
2930        };
2931
2932        // Create base relation
2933        let buffer = create_test_buffer(&executor, &[1, 2, 3], "key");
2934        executor.store_mut().put("base_rel", buffer);
2935        executor.register_relation(RelId(1), "base_rel");
2936
2937        // Create an empty recursive relation (simulating a recursive step that produces nothing)
2938        let empty_schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2939        let empty_buffer = executor.create_empty_buffer(empty_schema).unwrap();
2940        executor.store_mut().put("empty_rel", empty_buffer);
2941        executor.register_relation(RelId(4), "empty_rel");
2942
2943        // Base: scan base_rel
2944        // Recursive: scan empty_rel (produces nothing new)
2945        let base = Box::new(RirNode::Scan { rel: RelId(1) });
2946        let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
2947
2948        let node = RirNode::Fixpoint {
2949            scc_id: 0,
2950            base,
2951            recursive,
2952            delta_rel: RelId(2),
2953            full_rel: RelId(3),
2954        };
2955
2956        let result = executor.execute_node(&node);
2957        assert!(result.is_ok());
2958
2959        // Should return base case since recursive produces nothing
2960        let result = result.unwrap();
2961        assert_eq!(buffer_row_count(&executor, &result), 3);
2962        let values = read_buffer_u32(&executor, &result, 0);
2963        assert_eq!(values, vec![1, 2, 3]);
2964    }
2965
2966    #[test]
2967    fn test_execute_fixpoint_empty_base() {
2968        // Test fixpoint with empty base case
2969        let mut executor = match create_test_executor() {
2970            Some(e) => e,
2971            None => {
2972                eprintln!("Skipping test: no CUDA device available");
2973                return;
2974            }
2975        };
2976
2977        // Create empty base relation
2978        let empty_schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
2979        let empty_buffer = executor.create_empty_buffer(empty_schema.clone()).unwrap();
2980        executor.store_mut().put("empty_base", empty_buffer);
2981        executor.register_relation(RelId(1), "empty_base");
2982
2983        // Create recursive relation (won't be used since base is empty)
2984        let rec_buffer = create_test_buffer(&executor, &[4, 5, 6], "key");
2985        executor.store_mut().put("rec_rel", rec_buffer);
2986        executor.register_relation(RelId(4), "rec_rel");
2987
2988        let base = Box::new(RirNode::Scan { rel: RelId(1) });
2989        let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
2990
2991        let node = RirNode::Fixpoint {
2992            scc_id: 0,
2993            base,
2994            recursive,
2995            delta_rel: RelId(2),
2996            full_rel: RelId(3),
2997        };
2998
2999        let result = executor.execute_node(&node);
3000        assert!(result.is_ok());
3001
3002        // Should return empty since base is empty
3003        let result = result.unwrap();
3004        assert_eq!(buffer_row_count(&executor, &result), 0);
3005    }
3006
3007    #[test]
3008    fn test_execute_fixpoint_one_iteration() {
3009        // Test fixpoint that converges after one iteration
3010        let mut executor = match create_test_executor() {
3011            Some(e) => e,
3012            None => {
3013                eprintln!("Skipping test: no CUDA device available");
3014                return;
3015            }
3016        };
3017
3018        // Base: [1, 2]
3019        let base_buffer = create_test_buffer(&executor, &[1, 2], "key");
3020        executor.store_mut().put("base_rel", base_buffer);
3021        executor.register_relation(RelId(1), "base_rel");
3022
3023        // Recursive produces [1, 2, 3] - after diff with R, only [3] remains
3024        let rec_buffer = create_test_buffer(&executor, &[1, 2, 3], "key");
3025        executor.store_mut().put("rec_rel", rec_buffer);
3026        executor.register_relation(RelId(4), "rec_rel");
3027
3028        // After first iteration, R = [1, 2, 3], recursive produces [1, 2, 3] again
3029        // diff([1, 2, 3], [1, 2, 3]) = empty -> fixpoint reached
3030
3031        let base = Box::new(RirNode::Scan { rel: RelId(1) });
3032        let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
3033
3034        let node = RirNode::Fixpoint {
3035            scc_id: 0,
3036            base,
3037            recursive,
3038            delta_rel: RelId(2),
3039            full_rel: RelId(3),
3040        };
3041
3042        let result = executor.execute_node(&node);
3043        assert!(result.is_ok());
3044
3045        let result = result.unwrap();
3046        // Result should be [1, 2, 3]
3047        assert_eq!(buffer_row_count(&executor, &result), 3);
3048    }
3049
3050    #[test]
3051    fn test_execute_fixpoint_multiple_iterations() {
3052        // Test fixpoint that requires multiple iterations to converge
3053        // This simulates transitive closure behavior
3054        let mut executor = match create_test_executor() {
3055            Some(e) => e,
3056            None => {
3057                eprintln!("Skipping test: no CUDA device available");
3058                return;
3059            }
3060        };
3061
3062        // Base: [1]
3063        let base_buffer = create_test_buffer(&executor, &[1], "key");
3064        executor.store_mut().put("base_rel", base_buffer);
3065        executor.register_relation(RelId(1), "base_rel");
3066
3067        // For this test, we need a recursive rule that can expand
3068        // Since we can't easily simulate join-based recursion without complex setup,
3069        // we'll test a simpler case where recursive produces cumulative data
3070
3071        // Recursive relation will produce [1, 2] in first iteration,
3072        // then [1, 2, 3] in second iteration, etc.
3073        // This requires a more complex setup, so let's test the basic convergence
3074
3075        // Simplified test: recursive produces union of base with [2]
3076        // First iteration: R=[1], rec produces [1, 2] -> delta_new = [2]
3077        // Second iteration: R=[1, 2], rec produces [1, 2] -> delta_new = empty
3078        let rec_buffer = create_test_buffer(&executor, &[1, 2], "key");
3079        executor.store_mut().put("rec_rel", rec_buffer);
3080        executor.register_relation(RelId(4), "rec_rel");
3081
3082        let base = Box::new(RirNode::Scan { rel: RelId(1) });
3083        let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
3084
3085        let node = RirNode::Fixpoint {
3086            scc_id: 0,
3087            base,
3088            recursive,
3089            delta_rel: RelId(2),
3090            full_rel: RelId(3),
3091        };
3092
3093        let result = executor.execute_node(&node);
3094        assert!(result.is_ok());
3095
3096        let result = result.unwrap();
3097        // Result should be union of [1] and [2] = [1, 2]
3098        assert_eq!(buffer_row_count(&executor, &result), 2);
3099    }
3100
3101    #[test]
3102    fn test_execute_fixpoint_via_node() {
3103        // Test fixpoint through execute_node to ensure the match arm works
3104        let mut executor = match create_test_executor() {
3105            Some(e) => e,
3106            None => {
3107                eprintln!("Skipping test: no CUDA device available");
3108                return;
3109            }
3110        };
3111
3112        // Create and store a base buffer
3113        let buffer = create_test_buffer(&executor, &[1, 2, 3], "key");
3114        executor.store_mut().put("base_rel", buffer);
3115        executor.register_relation(RelId(1), "base_rel");
3116
3117        // Empty recursive means immediate fixpoint
3118        let empty_schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
3119        let empty_buffer = executor.create_empty_buffer(empty_schema).unwrap();
3120        executor.store_mut().put("empty_rel", empty_buffer);
3121        executor.register_relation(RelId(4), "empty_rel");
3122
3123        let base = Box::new(RirNode::Scan { rel: RelId(1) });
3124        let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
3125
3126        let node = RirNode::Fixpoint {
3127            scc_id: 0,
3128            base,
3129            recursive,
3130            delta_rel: RelId(2),
3131            full_rel: RelId(3),
3132        };
3133
3134        let result = executor.execute_node(&node);
3135        assert!(result.is_ok());
3136
3137        let result = result.unwrap();
3138        assert_eq!(buffer_row_count(&executor, &result), 3);
3139    }
3140
3141    #[test]
3142    fn test_fixpoint_cleanup() {
3143        // Test that fixpoint properly cleans up delta and full relations
3144        let mut executor = match create_test_executor() {
3145            Some(e) => e,
3146            None => {
3147                eprintln!("Skipping test: no CUDA device available");
3148                return;
3149            }
3150        };
3151
3152        let buffer = create_test_buffer(&executor, &[1, 2], "key");
3153        executor.store_mut().put("base_rel", buffer);
3154        executor.register_relation(RelId(1), "base_rel");
3155
3156        let empty_schema = Schema::new(vec![("key".to_string(), ScalarType::U32)]);
3157        let empty_buffer = executor.create_empty_buffer(empty_schema).unwrap();
3158        executor.store_mut().put("empty_rel", empty_buffer);
3159        executor.register_relation(RelId(4), "empty_rel");
3160
3161        // Register names for delta and full relations to check cleanup
3162        executor.register_relation(RelId(2), "__delta_test");
3163        executor.register_relation(RelId(3), "__full_test");
3164
3165        let base = Box::new(RirNode::Scan { rel: RelId(1) });
3166        let recursive = Box::new(RirNode::Scan { rel: RelId(4) });
3167
3168        let node = RirNode::Fixpoint {
3169            scc_id: 0,
3170            base,
3171            recursive,
3172            delta_rel: RelId(2),
3173            full_rel: RelId(3),
3174        };
3175
3176        let result = executor.execute_node(&node);
3177        assert!(result.is_ok());
3178
3179        // After fixpoint, the delta and full relations should be cleaned up
3180        assert!(!executor.store().contains("__delta_test"));
3181        assert!(!executor.store().contains("__full_test"));
3182    }
3183
3184    // ============== Execute Plan Tests ==============
3185
3186    #[test]
3187    fn test_execute_plan_empty() {
3188        let mut executor = match create_test_executor() {
3189            Some(e) => e,
3190            None => {
3191                eprintln!("Skipping test: no CUDA device available");
3192                return;
3193            }
3194        };
3195
3196        let plan = ExecutionPlan::new(vec![]);
3197
3198        let result = executor.execute_plan(&plan);
3199        assert!(result.is_ok());
3200        let result = result.unwrap();
3201        assert_eq!(buffer_row_count(&executor, &result), 0);
3202    }
3203
3204    #[test]
3205    fn test_execute_plan_with_stratum() {
3206        let mut executor = match create_test_executor() {
3207            Some(e) => e,
3208            None => {
3209                eprintln!("Skipping test: no CUDA device available");
3210                return;
3211            }
3212        };
3213
3214        // Create input relation
3215        let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
3216        executor.store_mut().put("input", buffer);
3217        executor.register_relation(RelId(1), "input");
3218
3219        // Build a simple plan
3220        let scc = Scc {
3221            id: 0,
3222            predicates: vec!["output".to_string()],
3223            is_recursive: false,
3224        };
3225
3226        let rule = CompiledRule {
3227            head: "output".to_string(),
3228            body: RirNode::Scan { rel: RelId(1) },
3229            meta: RirMeta::default(),
3230        };
3231
3232        let stratum = Stratum {
3233            id: 0,
3234            sccs: vec![0],
3235        };
3236
3237        let plan = ExecutionPlan {
3238            sccs: vec![scc],
3239            strata: vec![stratum],
3240            rules_by_scc: vec![vec![rule]],
3241            generated_query_rules: vec![],
3242            est_memory_peak: 0,
3243            rel_arities: std::collections::HashMap::new(),
3244        };
3245
3246        let result = executor.execute_plan(&plan);
3247        assert!(result.is_ok());
3248
3249        // Verify output relation was created
3250        assert!(executor.store().contains("output"));
3251        let output = executor.store().get("output").unwrap();
3252        assert_eq!(buffer_row_count(&executor, output), 5);
3253    }
3254
3255    #[test]
3256    fn test_apply_deltas_and_recompute_updates_dependents() {
3257        let mut executor = match create_test_executor() {
3258            Some(e) => e,
3259            None => {
3260                eprintln!("Skipping test: no CUDA device available");
3261                return;
3262            }
3263        };
3264
3265        let input = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
3266        executor.store_mut().put("input", input);
3267        executor.register_relation(RelId(1), "input");
3268
3269        // SCC0: identity rule for input (mirrors how compiled facts appear as scan rules).
3270        // SCC1: output depends on input.
3271        let scc0 = Scc {
3272            id: 0,
3273            predicates: vec!["input".to_string()],
3274            is_recursive: false,
3275        };
3276        let scc1 = Scc {
3277            id: 1,
3278            predicates: vec!["output".to_string()],
3279            is_recursive: false,
3280        };
3281
3282        let input_rule = CompiledRule {
3283            head: "input".to_string(),
3284            body: RirNode::Scan { rel: RelId(1) },
3285            meta: RirMeta::default(),
3286        };
3287
3288        let output_rule = CompiledRule {
3289            head: "output".to_string(),
3290            body: RirNode::Filter {
3291                input: Box::new(RirNode::Scan { rel: RelId(1) }),
3292                predicate: Expr::Compare {
3293                    left: Box::new(Expr::Column(0)),
3294                    op: CompareOp::Gt,
3295                    right: Box::new(Expr::Const(ConstValue::U32(2))),
3296                },
3297            },
3298            meta: RirMeta::default(),
3299        };
3300
3301        let stratum = Stratum {
3302            id: 0,
3303            sccs: vec![0, 1],
3304        };
3305
3306        let plan = ExecutionPlan {
3307            sccs: vec![scc0, scc1],
3308            strata: vec![stratum],
3309            rules_by_scc: vec![vec![input_rule], vec![output_rule]],
3310            generated_query_rules: vec![],
3311            est_memory_peak: 0,
3312            rel_arities: std::collections::HashMap::new(),
3313        };
3314
3315        executor.execute_plan(&plan).expect("initial execute_plan");
3316        let initial_out = executor.store().get("output").expect("output missing");
3317        let initial_vals = read_buffer_u32(&executor, initial_out, 0);
3318        assert_eq!(initial_vals, vec![3, 4, 5]);
3319
3320        let delete_buf = create_test_buffer(&executor, &[5], "key");
3321        let insert_buf = create_test_buffer(&executor, &[10], "key");
3322
3323        let mut deltas = HashMap::new();
3324        deltas.insert(
3325            "input".to_string(),
3326            RelationDelta::new(Some(insert_buf), Some(delete_buf)),
3327        );
3328
3329        executor
3330            .apply_deltas_and_recompute(&plan, &deltas)
3331            .expect("apply_deltas_and_recompute");
3332
3333        let out = executor
3334            .store()
3335            .get("output")
3336            .expect("output missing after recompute");
3337        let vals = read_buffer_u32(&executor, out, 0);
3338        assert_eq!(vals, vec![3, 4, 10]);
3339    }
3340
3341    #[test]
3342    fn test_apply_deltas_and_recompute_insert_only_recomputes_anti_join() {
3343        let mut executor = match create_test_executor() {
3344            Some(e) => e,
3345            None => {
3346                eprintln!("Skipping test: no CUDA device available");
3347                return;
3348            }
3349        };
3350
3351        let lhs = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
3352        executor.store_mut().put("lhs", lhs);
3353        executor.register_relation(RelId(1), "lhs");
3354
3355        let blocked = create_test_buffer(&executor, &[], "key");
3356        executor.store_mut().put("blocked", blocked);
3357        executor.register_relation(RelId(2), "blocked");
3358
3359        // SCC0: lhs identity rule
3360        // SCC1: blocked identity rule
3361        // SCC2: out = lhs \ blocked (anti-join)
3362        let scc0 = Scc {
3363            id: 0,
3364            predicates: vec!["lhs".to_string()],
3365            is_recursive: false,
3366        };
3367        let scc1 = Scc {
3368            id: 1,
3369            predicates: vec!["blocked".to_string()],
3370            is_recursive: false,
3371        };
3372        let scc2 = Scc {
3373            id: 2,
3374            predicates: vec!["out".to_string()],
3375            is_recursive: false,
3376        };
3377
3378        let lhs_rule = CompiledRule {
3379            head: "lhs".to_string(),
3380            body: RirNode::Scan { rel: RelId(1) },
3381            meta: RirMeta::default(),
3382        };
3383        let blocked_rule = CompiledRule {
3384            head: "blocked".to_string(),
3385            body: RirNode::Scan { rel: RelId(2) },
3386            meta: RirMeta::default(),
3387        };
3388        let out_rule = CompiledRule {
3389            head: "out".to_string(),
3390            body: RirNode::Join {
3391                left: Box::new(RirNode::Scan { rel: RelId(1) }),
3392                right: Box::new(RirNode::Scan { rel: RelId(2) }),
3393                left_keys: vec![0],
3394                right_keys: vec![0],
3395                join_type: JoinType::Anti,
3396            },
3397            meta: RirMeta::default(),
3398        };
3399
3400        let stratum = Stratum {
3401            id: 0,
3402            sccs: vec![0, 1, 2],
3403        };
3404
3405        let plan = ExecutionPlan {
3406            sccs: vec![scc0, scc1, scc2],
3407            strata: vec![stratum],
3408            rules_by_scc: vec![vec![lhs_rule], vec![blocked_rule], vec![out_rule]],
3409            generated_query_rules: vec![],
3410            est_memory_peak: 0,
3411            rel_arities: std::collections::HashMap::new(),
3412        };
3413
3414        executor.execute_plan(&plan).expect("initial execute_plan");
3415        let initial = executor.store().get("out").expect("out missing");
3416        let initial_vals = read_buffer_u32(&executor, initial, 0);
3417        assert_eq!(initial_vals, vec![1, 2, 3, 4, 5]);
3418
3419        // Insert into the "blocked" relation: output should shrink.
3420        let insert_buf = create_test_buffer(&executor, &[2, 4], "key");
3421        let mut deltas = HashMap::new();
3422        deltas.insert(
3423            "blocked".to_string(),
3424            RelationDelta::new(Some(insert_buf), None),
3425        );
3426
3427        executor
3428            .apply_deltas_and_recompute(&plan, &deltas)
3429            .expect("apply_deltas_and_recompute");
3430
3431        let out = executor
3432            .store()
3433            .get("out")
3434            .expect("out missing after update");
3435        let vals = read_buffer_u32(&executor, out, 0);
3436        assert_eq!(vals, vec![1, 3, 5]);
3437    }
3438
3439    // ============== RIR Node Composition Tests ==============
3440
3441    #[test]
3442    fn test_execute_filter_project_chain() {
3443        let mut executor = match create_test_executor() {
3444            Some(e) => e,
3445            None => {
3446                eprintln!("Skipping test: no CUDA device available");
3447                return;
3448            }
3449        };
3450
3451        // Create input relation
3452        let buffer = create_test_buffer(&executor, &[1, 2, 3, 4, 5], "key");
3453        executor.store_mut().put("input", buffer);
3454        executor.register_relation(RelId(1), "input");
3455
3456        // Build: Project(Filter(Scan))
3457        let scan = RirNode::Scan { rel: RelId(1) };
3458        let filter = RirNode::Filter {
3459            input: Box::new(scan),
3460            predicate: Expr::Compare {
3461                left: Box::new(Expr::Column(0)),
3462                op: CompareOp::Gt,
3463                right: Box::new(Expr::Const(ConstValue::U32(2))),
3464            },
3465        };
3466        let project = RirNode::Project {
3467            input: Box::new(filter),
3468            columns: vec![ProjectExpr::Column(0)],
3469        };
3470
3471        let result = executor.execute_node(&project);
3472        assert!(result.is_ok());
3473
3474        let result = result.unwrap();
3475        assert_eq!(buffer_row_count(&executor, &result), 3);
3476
3477        let values = read_buffer_u32(&executor, &result, 0);
3478        assert_eq!(values, vec![3, 4, 5]);
3479    }
3480
3481    #[test]
3482    fn test_project_profiling_observes_input_and_output_allocations() {
3483        let mut executor = match create_test_executor_with_config(
3484            RuntimeConfig::default().with_common_subexpression_elimination(Some(false)),
3485        ) {
3486            Some(executor) => executor,
3487            None => {
3488                eprintln!("Skipping test: no CUDA device available");
3489                return;
3490            }
3491        };
3492        let buffer = create_test_buffer(&executor, &[1, 2, 3, 4], "key");
3493        executor.store_mut().put("input", buffer);
3494        executor.register_relation(RelId(1), "input");
3495        let columns = vec![ProjectExpr::Column(0)];
3496
3497        let baseline = executor.provider.memory().allocated_bytes();
3498        let input = executor.execute_scan(RelId(1)).expect("clone scan input");
3499        let output = executor
3500            .execute_project(&input, &columns)
3501            .expect("project while retaining input");
3502        let expected_observation = executor.provider.memory().allocated_bytes();
3503        drop(output);
3504        drop(input);
3505        assert_eq!(executor.provider.memory().allocated_bytes(), baseline);
3506
3507        executor.set_profiling(true);
3508        let node = RirNode::Project {
3509            input: Box::new(RirNode::Scan { rel: RelId(1) }),
3510            columns,
3511        };
3512        let _result = executor
3513            .execute_node(&node)
3514            .expect("execute profiled project");
3515        let observed_peak = executor.execution_stats(0).peak_memory_bytes;
3516
3517        assert!(
3518            observed_peak >= expected_observation,
3519            "profiled project peak {observed_peak} omitted live input allocation; expected at least {expected_observation}"
3520        );
3521    }
3522
3523    // ============== Common Subexpression Elimination Tests ==============
3524
3525    fn duplicate_join_union_plan() -> RirNode {
3526        let join = RirNode::Join {
3527            left: Box::new(RirNode::Scan { rel: RelId(1) }),
3528            right: Box::new(RirNode::Scan { rel: RelId(2) }),
3529            left_keys: vec![0],
3530            right_keys: vec![0],
3531            join_type: JoinType::Inner,
3532        };
3533        RirNode::Union {
3534            inputs: vec![join.clone(), join],
3535        }
3536    }
3537
3538    fn seed_cse_join_fixture(executor: &mut Executor, right: &[u32]) {
3539        executor.register_relation(RelId(1), "left");
3540        executor.register_relation(RelId(2), "right");
3541        let left = create_test_buffer(executor, &[1, 2, 3, 4], "key");
3542        let right = create_test_buffer(executor, right, "key");
3543        executor.put_relation("left", left);
3544        executor.put_relation("right", right);
3545    }
3546
3547    #[test]
3548    fn test_common_subexpression_cache_reuses_duplicate_inner_join_when_enabled() {
3549        let mut executor = match create_test_executor_with_config(
3550            RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3551        ) {
3552            Some(e) => e,
3553            None => {
3554                eprintln!("Skipping test: no CUDA device available");
3555                return;
3556            }
3557        };
3558        seed_cse_join_fixture(&mut executor, &[2, 3, 5]);
3559
3560        let result = executor
3561            .execute_node(&duplicate_join_union_plan())
3562            .expect("duplicate join union executes");
3563
3564        assert_eq!(buffer_row_count(&executor, &result), 2);
3565        let stats = executor.common_subexpression_stats();
3566        assert_eq!(stats.hits, 1);
3567        assert!(stats.misses >= 1);
3568        assert_eq!(stats.unsafe_rejections, 0);
3569    }
3570
3571    #[test]
3572    fn test_common_subexpression_cache_treats_project_chains_atomically() {
3573        let mut executor = match create_test_executor_with_config(
3574            RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3575        ) {
3576            Some(executor) => executor,
3577            None => {
3578                eprintln!("Skipping test: no CUDA device available");
3579                return;
3580            }
3581        };
3582        executor.register_relation(RelId(1), "input");
3583        let input = create_test_buffer(&executor, &[1, 2, 3], "key");
3584        executor.put_relation("input", input);
3585
3586        let inner = RirNode::Project {
3587            input: Box::new(RirNode::Scan { rel: RelId(1) }),
3588            columns: vec![ProjectExpr::Column(0)],
3589        };
3590        let passthrough = RirNode::Project {
3591            input: Box::new(inner.clone()),
3592            columns: vec![ProjectExpr::Column(0)],
3593        };
3594        let computed = RirNode::Project {
3595            input: Box::new(inner),
3596            columns: vec![ProjectExpr::Computed(
3597                Expr::Add(
3598                    Box::new(Expr::Column(0)),
3599                    Box::new(Expr::Const(ConstValue::U32(1))),
3600                ),
3601                ScalarType::U32,
3602            )],
3603        };
3604
3605        executor
3606            .execute_node(&passthrough)
3607            .expect("first project chain executes");
3608        let result = executor
3609            .execute_node(&computed)
3610            .expect("second project chain executes");
3611
3612        assert_eq!(read_buffer_u32(&executor, &result, 0), vec![2, 3, 4]);
3613        executor
3614            .execute_node(&computed)
3615            .expect("duplicate full project chain executes");
3616        let stats = executor.common_subexpression_stats();
3617        assert_eq!(stats.hits, 1);
3618        assert_eq!(stats.misses, 2);
3619    }
3620
3621    #[test]
3622    fn test_common_subexpression_off_on_preserves_output_and_records_reuse_only_when_enabled() {
3623        let mut disabled = match create_test_executor_with_config(
3624            RuntimeConfig::default().with_common_subexpression_elimination(Some(false)),
3625        ) {
3626            Some(e) => e,
3627            None => {
3628                eprintln!("Skipping test: no CUDA device available");
3629                return;
3630            }
3631        };
3632        let mut enabled = match create_test_executor_with_config(
3633            RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3634        ) {
3635            Some(e) => e,
3636            None => {
3637                eprintln!("Skipping test: no CUDA device available");
3638                return;
3639            }
3640        };
3641        seed_cse_join_fixture(&mut disabled, &[2, 3, 5]);
3642        seed_cse_join_fixture(&mut enabled, &[2, 3, 5]);
3643        let plan = duplicate_join_union_plan();
3644
3645        disabled.provider.reset_d2h_transfer_count();
3646        enabled.provider.reset_d2h_transfer_count();
3647        let disabled_result = disabled.execute_node(&plan).expect("disabled CSE output");
3648        let enabled_result = enabled.execute_node(&plan).expect("enabled CSE output");
3649        let disabled_d2h = disabled.provider.d2h_transfer_count();
3650        let enabled_d2h = enabled.provider.d2h_transfer_count();
3651
3652        assert_eq!(
3653            read_buffer_u32(&disabled, &disabled_result, 0),
3654            read_buffer_u32(&enabled, &enabled_result, 0)
3655        );
3656        assert_eq!(enabled_d2h, disabled_d2h);
3657        assert_eq!(disabled.common_subexpression_stats().hits, 0);
3658        assert_eq!(disabled.common_subexpression_stats().misses, 0);
3659        assert_eq!(enabled.common_subexpression_stats().hits, 1);
3660    }
3661
3662    #[test]
3663    fn test_common_subexpression_cache_invalidates_on_relation_generation_change() {
3664        let mut executor = match create_test_executor_with_config(
3665            RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3666        ) {
3667            Some(e) => e,
3668            None => {
3669                eprintln!("Skipping test: no CUDA device available");
3670                return;
3671            }
3672        };
3673        seed_cse_join_fixture(&mut executor, &[2, 3, 5]);
3674        let plan = duplicate_join_union_plan();
3675
3676        executor.execute_node(&plan).expect("first execution");
3677        assert_eq!(executor.common_subexpression_stats().hits, 1);
3678
3679        let changed_right = create_test_buffer(&executor, &[4], "key");
3680        executor.put_relation("right", changed_right);
3681        let result = executor.execute_node(&plan).expect("second execution");
3682
3683        assert_eq!(buffer_row_count(&executor, &result), 1);
3684        let stats = executor.common_subexpression_stats();
3685        assert_eq!(stats.hits, 2);
3686        assert!(stats.misses >= 2);
3687    }
3688
3689    #[test]
3690    fn test_common_subexpression_cache_rejects_unsafe_difference_boundary() {
3691        let mut executor = match create_test_executor_with_config(
3692            RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3693        ) {
3694            Some(e) => e,
3695            None => {
3696                eprintln!("Skipping test: no CUDA device available");
3697                return;
3698            }
3699        };
3700        seed_cse_join_fixture(&mut executor, &[2, 3, 5]);
3701        let diff = RirNode::Diff {
3702            left: Box::new(RirNode::Scan { rel: RelId(1) }),
3703            right: Box::new(RirNode::Scan { rel: RelId(2) }),
3704        };
3705
3706        executor
3707            .execute_node(&RirNode::Union {
3708                inputs: vec![diff.clone(), diff],
3709            })
3710            .expect("unsafe duplicate diff still executes without CSE sharing");
3711
3712        let stats = executor.common_subexpression_stats();
3713        assert_eq!(stats.hits, 0);
3714        assert!(stats.unsafe_rejections >= 1);
3715        assert!(stats
3716            .rejection_reasons
3717            .iter()
3718            .any(|reason| reason == "negation_or_difference_boundary"));
3719    }
3720
3721    #[test]
3722    fn test_common_subexpression_key_rejects_aggregate_and_tensor_boundaries() {
3723        let mut executor = match create_test_executor_with_config(
3724            RuntimeConfig::default().with_common_subexpression_elimination(Some(true)),
3725        ) {
3726            Some(e) => e,
3727            None => {
3728                eprintln!("Skipping test: no CUDA device available");
3729                return;
3730            }
3731        };
3732        let aggregate = RirNode::GroupBy {
3733            input: Box::new(RirNode::Scan { rel: RelId(1) }),
3734            key_cols: vec![0],
3735            aggs: vec![(0, xlog_core::AggOp::Count)],
3736        };
3737        let tensor = RirNode::TensorMaskedJoin {
3738            mask_name: "W".to_string(),
3739            schema_size: 1,
3740            left_keys: vec![0],
3741            right_keys: vec![0],
3742            rel_index: vec![(RelId(1), "left".to_string())],
3743            head_rel_name: "head".to_string(),
3744            head_rel_id: RelId(3),
3745            max_active_rules: 1,
3746            head_projection: vec![0],
3747        };
3748        let chain = RirNode::ChainJoin {
3749            left: Box::new(RirNode::Scan { rel: RelId(1) }),
3750            right: Box::new(RirNode::Scan { rel: RelId(2) }),
3751            left_key: 0,
3752            right_key: 0,
3753            output_columns: vec![ProjectExpr::Column(0)],
3754            fallback: Box::new(RirNode::Join {
3755                left: Box::new(RirNode::Scan { rel: RelId(1) }),
3756                right: Box::new(RirNode::Scan { rel: RelId(2) }),
3757                left_keys: vec![0],
3758                right_keys: vec![0],
3759                join_type: JoinType::Inner,
3760            }),
3761        };
3762
3763        assert!(executor.common_subexpression_key(&aggregate).is_none());
3764        assert!(executor.common_subexpression_key(&tensor).is_none());
3765        assert!(executor.common_subexpression_key(&chain).is_none());
3766
3767        let reasons = &executor.common_subexpression_stats().rejection_reasons;
3768        assert!(reasons.iter().any(|reason| reason == "aggregate_boundary"));
3769        assert!(reasons
3770            .iter()
3771            .any(|reason| reason == "provenance_or_tensor_boundary"));
3772        assert!(reasons
3773            .iter()
3774            .any(|reason| reason == "specialized_dispatch_boundary"));
3775    }
3776
3777    // ============== Adaptive Runtime Re-Optimization Tests ==============
3778
3779    fn adaptive_scc() -> Scc {
3780        Scc {
3781            id: 0,
3782            predicates: vec!["out".to_string()],
3783            is_recursive: false,
3784        }
3785    }
3786
3787    fn adaptive_stratum() -> Stratum {
3788        Stratum {
3789            id: 0,
3790            sccs: vec![0],
3791        }
3792    }
3793
3794    fn adaptive_rule(body: RirNode) -> CompiledRule {
3795        CompiledRule {
3796            head: "out".to_string(),
3797            body,
3798            meta: RirMeta::default(),
3799        }
3800    }
3801
3802    fn adaptive_plan(body: RirNode) -> ExecutionPlan {
3803        ExecutionPlan {
3804            sccs: vec![adaptive_scc()],
3805            strata: vec![adaptive_stratum()],
3806            rules_by_scc: vec![vec![adaptive_rule(body)]],
3807            generated_query_rules: vec![],
3808            est_memory_peak: 0,
3809            rel_arities: std::collections::HashMap::new(),
3810        }
3811    }
3812
3813    fn adaptive_baseline_join_plan() -> ExecutionPlan {
3814        adaptive_plan(RirNode::Project {
3815            input: Box::new(RirNode::Join {
3816                left: Box::new(RirNode::Scan { rel: RelId(1) }),
3817                right: Box::new(RirNode::Scan { rel: RelId(2) }),
3818                left_keys: vec![0],
3819                right_keys: vec![0],
3820                join_type: JoinType::Inner,
3821            }),
3822            columns: vec![ProjectExpr::Column(0)],
3823        })
3824    }
3825
3826    fn adaptive_scan_candidate_plan(rel: RelId) -> ExecutionPlan {
3827        adaptive_plan(RirNode::Scan { rel })
3828    }
3829
3830    fn seed_adaptive_fixture(executor: &mut Executor, right: &[u32]) {
3831        executor.register_relation(RelId(1), "left");
3832        executor.register_relation(RelId(2), "right");
3833        let left = create_test_buffer(executor, &[1, 2, 3, 4, 5, 6, 7, 8], "key");
3834        let right = create_test_buffer(executor, right, "key");
3835        executor.put_relation("left", left);
3836        executor.put_relation("right", right);
3837    }
3838
3839    #[test]
3840    fn test_adaptive_reoptimization_disabled_uses_baseline_and_records_decision() {
3841        let mut executor = match create_test_executor_with_config(
3842            RuntimeConfig::default().with_adaptive_reoptimization(Some(false)),
3843        ) {
3844            Some(e) => e,
3845            None => {
3846                eprintln!("Skipping test: no CUDA device available");
3847                return;
3848            }
3849        };
3850        seed_adaptive_fixture(&mut executor, &[1, 2, 3, 4, 5, 6, 7, 8]);
3851
3852        let baseline = adaptive_baseline_join_plan();
3853        let candidate = adaptive_scan_candidate_plan(RelId(2));
3854        let result = executor
3855            .execute_plan_with_adaptive_candidate(&baseline, &candidate)
3856            .expect("disabled adaptation executes baseline");
3857
3858        assert_eq!(
3859            read_buffer_u32(&executor, &result, 0),
3860            (1..=8).collect::<Vec<_>>()
3861        );
3862        let stats = executor.adaptive_reoptimization_stats();
3863        assert_eq!(stats.disabled, 1);
3864        assert_eq!(stats.adopted, 0);
3865        assert_eq!(stats.rolled_back, 0);
3866        assert_eq!(
3867            stats.last_decision.as_ref().map(|decision| decision.action),
3868            Some(AdaptiveReoptimizationAction::Disabled)
3869        );
3870    }
3871
3872    #[test]
3873    fn test_adaptive_reoptimization_adopts_equivalent_candidate_and_records_telemetry() {
3874        let mut executor = match create_test_executor_with_config(
3875            RuntimeConfig::default().with_adaptive_reoptimization(Some(true)),
3876        ) {
3877            Some(e) => e,
3878            None => {
3879                eprintln!("Skipping test: no CUDA device available");
3880                return;
3881            }
3882        };
3883        seed_adaptive_fixture(&mut executor, &[1, 2, 3, 4, 5, 6, 7, 8]);
3884        executor.provider.reset_host_transfer_stats();
3885
3886        let baseline = adaptive_baseline_join_plan();
3887        let candidate = adaptive_scan_candidate_plan(RelId(1));
3888        let result = executor
3889            .execute_plan_with_adaptive_candidate(&baseline, &candidate)
3890            .expect("equivalent candidate is adopted");
3891
3892        assert_eq!(
3893            read_buffer_u32(&executor, &result, 0),
3894            (1..=8).collect::<Vec<_>>()
3895        );
3896        let stats = executor.adaptive_reoptimization_stats();
3897        assert_eq!(stats.adopted, 1);
3898        assert_eq!(stats.rolled_back, 0);
3899        assert_eq!(stats.last_observations.len(), 1);
3900        assert!(stats.last_observations[0].cardinality_delta_abs > 0);
3901        assert!(stats.last_observations[0].selectivity_delta_abs > 0.0);
3902        assert_eq!(stats.data_plane_dtoh_calls, 0);
3903    }
3904
3905    #[test]
3906    fn test_adaptive_reoptimization_rolls_back_bad_candidate_with_typed_diagnostic() {
3907        let mut executor = match create_test_executor_with_config(
3908            RuntimeConfig::default().with_adaptive_reoptimization(Some(true)),
3909        ) {
3910            Some(e) => e,
3911            None => {
3912                eprintln!("Skipping test: no CUDA device available");
3913                return;
3914            }
3915        };
3916        seed_adaptive_fixture(&mut executor, &[1, 2, 3, 4, 5, 6, 7, 8]);
3917
3918        let baseline = adaptive_baseline_join_plan();
3919        let bad_candidate = adaptive_scan_candidate_plan(RelId(2));
3920        executor.put_relation("right", create_test_buffer(&executor, &[99], "key"));
3921        let result = executor
3922            .execute_plan_with_adaptive_candidate(&baseline, &bad_candidate)
3923            .expect("bad candidate rolls back to baseline output");
3924
3925        assert_eq!(read_buffer_u32(&executor, &result, 0), Vec::<u32>::new());
3926        let out = executor.store().get("out").expect("rollback restored out");
3927        assert_eq!(read_buffer_u32(&executor, out, 0), Vec::<u32>::new());
3928        let stats = executor.adaptive_reoptimization_stats();
3929        assert_eq!(stats.adopted, 0);
3930        assert_eq!(stats.rolled_back, 1);
3931        assert!(stats.diagnostics.iter().any(|diagnostic| {
3932            diagnostic.kind == AdaptiveReoptimizationDiagnosticKind::CandidateOutputMismatch
3933        }));
3934    }
3935
3936    #[test]
3937    fn test_adaptive_reoptimization_decisions_are_deterministic_under_replay() {
3938        let mut executor = match create_test_executor_with_config(
3939            RuntimeConfig::default().with_adaptive_reoptimization(Some(true)),
3940        ) {
3941            Some(e) => e,
3942            None => {
3943                eprintln!("Skipping test: no CUDA device available");
3944                return;
3945            }
3946        };
3947        seed_adaptive_fixture(&mut executor, &[1, 2, 3, 4, 5, 6, 7, 8]);
3948        let baseline = adaptive_baseline_join_plan();
3949        executor
3950            .execute_plan(&baseline)
3951            .expect("baseline execution records telemetry");
3952        let observations = executor
3953            .adaptive_reoptimization_stats()
3954            .last_observations
3955            .clone();
3956
3957        let first = executor.replay_adaptive_reoptimization_decision(&observations);
3958        for _ in 0..100 {
3959            assert_eq!(
3960                executor.replay_adaptive_reoptimization_decision(&observations),
3961                first
3962            );
3963        }
3964    }
3965
3966    // ============== Persistent Hash Index Manager Tests ==============
3967
3968    fn persistent_index_join_plan() -> ExecutionPlan {
3969        adaptive_baseline_join_plan()
3970    }
3971
3972    fn persistent_index_heavy_join_plan(repetitions: usize) -> ExecutionPlan {
3973        let mut inputs = Vec::with_capacity(repetitions);
3974        for _ in 0..repetitions {
3975            inputs.push(RirNode::Project {
3976                input: Box::new(RirNode::Join {
3977                    left: Box::new(RirNode::Scan { rel: RelId(1) }),
3978                    right: Box::new(RirNode::Scan { rel: RelId(2) }),
3979                    left_keys: vec![0],
3980                    right_keys: vec![0],
3981                    join_type: JoinType::Semi,
3982                }),
3983                columns: vec![ProjectExpr::Column(0)],
3984            });
3985        }
3986        adaptive_plan(RirNode::Union { inputs })
3987    }
3988
3989    fn seed_persistent_index_fixture(executor: &mut Executor, rows: u32) {
3990        executor.register_relation(RelId(1), "left");
3991        executor.register_relation(RelId(2), "right");
3992        let values: Vec<u32> = (0..rows).collect();
3993        let left = create_test_buffer(executor, &values, "key");
3994        let right = create_test_buffer(executor, &values, "key");
3995        executor.put_relation("left", left);
3996        executor.put_relation("right", right);
3997    }
3998
3999    fn seed_persistent_index_performance_fixture(
4000        executor: &mut Executor,
4001        left_rows: u32,
4002        right_rows: u32,
4003    ) {
4004        executor.register_relation(RelId(1), "left");
4005        executor.register_relation(RelId(2), "right");
4006        let left_values: Vec<u32> = (0..left_rows).collect();
4007        let right_values: Vec<u32> = (0..right_rows).collect();
4008        let left = create_test_buffer(executor, &left_values, "key");
4009        let right = create_test_buffer(executor, &right_values, "key");
4010        executor.put_relation("left", left);
4011        executor.put_relation("right", right);
4012    }
4013
4014    fn warm_persistent_index(executor: &mut Executor, plan: &ExecutionPlan, times: usize) {
4015        for _ in 0..times {
4016            executor.execute_plan(plan).expect("persistent index plan");
4017        }
4018    }
4019
4020    fn median_duration(samples: &mut [Duration]) -> Duration {
4021        samples.sort_unstable();
4022        samples[samples.len() / 2]
4023    }
4024
4025    fn measure_persistent_index_fixture(
4026        mut executor: Executor,
4027        plan: &ExecutionPlan,
4028        warmup: usize,
4029        iterations: usize,
4030    ) -> (
4031        Duration,
4032        u64,
4033        JoinIndexCacheStats,
4034        xlog_cuda::provider::HostTransferStats,
4035    ) {
4036        let mut output_rows = None;
4037        warm_persistent_index(&mut executor, plan, warmup);
4038        executor.provider.reset_host_transfer_stats();
4039
4040        let mut samples = Vec::with_capacity(iterations);
4041        for _ in 0..iterations {
4042            let start = Instant::now();
4043            let output = executor.execute_plan(plan).expect("persistent index plan");
4044            executor
4045                .provider
4046                .device()
4047                .synchronize()
4048                .expect("sync device");
4049            samples.push(start.elapsed());
4050            output_rows = Some(if let Some(buffer) = executor.store().get("out") {
4051                executor
4052                    .buffer_row_count(buffer)
4053                    .expect("read output row count")
4054                    .into()
4055            } else {
4056                output.num_rows()
4057            });
4058        }
4059
4060        (
4061            median_duration(&mut samples),
4062            output_rows.expect("at least one measured execution"),
4063            executor.join_index_cache_stats(),
4064            executor.provider.host_transfer_stats(),
4065        )
4066    }
4067
4068    #[test]
4069    fn test_persistent_hash_index_reuses_across_repeated_session_evaluations() {
4070        let mut executor = match create_test_executor_with_config(
4071            RuntimeConfig::default().with_persistent_hash_indexes(Some(true)),
4072        ) {
4073            Some(e) => e,
4074            None => {
4075                eprintln!("Skipping test: no CUDA device available");
4076                return;
4077            }
4078        };
4079        seed_persistent_index_fixture(&mut executor, 2_500);
4080        let plan = persistent_index_join_plan();
4081        executor.provider.reset_host_transfer_stats();
4082
4083        warm_persistent_index(&mut executor, &plan, 5);
4084
4085        let stats = executor.join_index_cache_stats();
4086        let transfers = executor.provider.host_transfer_stats();
4087        assert_eq!(stats.builds, 1);
4088        assert!(stats.hits >= 1);
4089        assert_eq!(stats.stale_rejections, 0);
4090        assert_eq!(stats.entries, 1);
4091        assert!(stats.total_bytes > 0);
4092        assert_eq!(transfers.dtoh_calls, 0);
4093        assert_eq!(transfers.htod_calls, 0);
4094    }
4095
4096    #[test]
4097    fn test_persistent_hash_index_invalidates_on_relation_generation_change() {
4098        let mut executor = match create_test_executor_with_config(
4099            RuntimeConfig::default().with_persistent_hash_indexes(Some(true)),
4100        ) {
4101            Some(e) => e,
4102            None => {
4103                eprintln!("Skipping test: no CUDA device available");
4104                return;
4105            }
4106        };
4107        seed_persistent_index_fixture(&mut executor, 2_500);
4108        let plan = persistent_index_join_plan();
4109        warm_persistent_index(&mut executor, &plan, 5);
4110        assert_eq!(executor.join_index_cache_stats().entries, 1);
4111
4112        let changed_values: Vec<u32> = (10_000..12_500).collect();
4113        let changed_right = create_test_buffer(&executor, &changed_values, "key");
4114        executor.put_relation("right", changed_right);
4115
4116        let stats = executor.join_index_cache_stats();
4117        assert_eq!(stats.entries, 0);
4118        assert!(stats.invalidations >= 1);
4119    }
4120
4121    #[test]
4122    fn test_persistent_hash_index_background_build_records_requests() {
4123        let mut executor = match create_test_executor_with_config(
4124            RuntimeConfig::default()
4125                .with_persistent_hash_indexes(Some(true))
4126                .with_persistent_hash_index_background_build(Some(true)),
4127        ) {
4128            Some(e) => e,
4129            None => {
4130                eprintln!("Skipping test: no CUDA device available");
4131                return;
4132            }
4133        };
4134        seed_persistent_index_fixture(&mut executor, 2_500);
4135        let plan = persistent_index_join_plan();
4136
4137        warm_persistent_index(&mut executor, &plan, 5);
4138
4139        let stats = executor.join_index_cache_stats();
4140        assert_eq!(stats.background_build_requests, 1);
4141        assert_eq!(stats.background_builds_completed, 1);
4142        assert_eq!(stats.entries, 1);
4143    }
4144
4145    #[test]
4146    fn test_persistent_hash_index_background_build_defers_current_join_reuse() {
4147        let mut executor = match create_test_executor_with_config(
4148            RuntimeConfig::default()
4149                .with_persistent_hash_indexes(Some(true))
4150                .with_persistent_hash_index_background_build(Some(true)),
4151        ) {
4152            Some(e) => e,
4153            None => {
4154                eprintln!("Skipping test: no CUDA device available");
4155                return;
4156            }
4157        };
4158        seed_persistent_index_fixture(&mut executor, 2_500);
4159        let plan = persistent_index_join_plan();
4160
4161        let mut before_build = executor.join_index_cache_stats();
4162        let mut after_build = None;
4163        for _ in 0..5 {
4164            executor
4165                .execute_plan(&plan)
4166                .expect("background-build warm evaluation");
4167            let stats = executor.join_index_cache_stats();
4168            if stats.background_build_requests > before_build.background_build_requests {
4169                after_build = Some(stats);
4170                break;
4171            }
4172            before_build = stats;
4173        }
4174
4175        let after_first = after_build.expect("background build request observed");
4176        assert_eq!(after_first.background_build_requests, 1);
4177        assert_eq!(after_first.background_builds_completed, 1);
4178        assert_eq!(after_first.background_builds_deferred, 1);
4179        assert_eq!(
4180            after_first.hits, before_build.hits,
4181            "background build must not be consumed by the same evaluation that requested it"
4182        );
4183        assert_eq!(after_first.entries, 1);
4184
4185        executor
4186            .execute_plan(&plan)
4187            .expect("second evaluation reuses completed background index");
4188        let after_second = executor.join_index_cache_stats();
4189        assert_eq!(after_second.background_build_requests, 1);
4190        assert_eq!(after_second.background_builds_deferred, 1);
4191        assert!(after_second.hits >= 1);
4192    }
4193
4194    #[test]
4195    fn test_persistent_hash_index_performance_fixture_meets_speedup_target() {
4196        const LEFT_ROWS: u32 = 8;
4197        const RIGHT_ROWS: u32 = 8_000_000;
4198        const JOIN_REPETITIONS: usize = 1;
4199        const WARMUP: usize = 12;
4200        const ITERATIONS: usize = 9;
4201
4202        let mut cached = match create_test_executor_with_config(
4203            RuntimeConfig::default().with_persistent_hash_indexes(Some(true)),
4204        ) {
4205            Some(e) => e,
4206            None => {
4207                eprintln!("Skipping test: no CUDA device available");
4208                return;
4209            }
4210        };
4211        seed_persistent_index_performance_fixture(&mut cached, LEFT_ROWS, RIGHT_ROWS);
4212
4213        let mut uncached = match create_test_executor_with_config(
4214            RuntimeConfig::default().with_persistent_hash_indexes(Some(false)),
4215        ) {
4216            Some(e) => e,
4217            None => {
4218                eprintln!("Skipping test: no CUDA device available");
4219                return;
4220            }
4221        };
4222        seed_persistent_index_performance_fixture(&mut uncached, LEFT_ROWS, RIGHT_ROWS);
4223
4224        let plan = persistent_index_heavy_join_plan(JOIN_REPETITIONS);
4225        let (cached_median, cached_rows, cached_stats, cached_transfers) =
4226            measure_persistent_index_fixture(cached, &plan, WARMUP, ITERATIONS);
4227        let (uncached_median, uncached_rows, uncached_stats, uncached_transfers) =
4228            measure_persistent_index_fixture(uncached, &plan, WARMUP, ITERATIONS);
4229
4230        let speedup_ratio = uncached_median.as_secs_f64() / cached_median.as_secs_f64();
4231        eprintln!(
4232            "persistent_hash_index_perf left_rows={} right_rows={} join_repetitions={} warmup={} iterations={} \
4233             cached_median_sec={:.9} uncached_median_sec={:.9} speedup_ratio={:.3} \
4234             cached_output_rows={} uncached_output_rows={} cached_builds={} cached_hits={} \
4235             uncached_builds={} cached_dtoh_calls={} cached_htod_calls={}",
4236            LEFT_ROWS,
4237            RIGHT_ROWS,
4238            JOIN_REPETITIONS,
4239            WARMUP,
4240            ITERATIONS,
4241            cached_median.as_secs_f64(),
4242            uncached_median.as_secs_f64(),
4243            speedup_ratio,
4244            cached_rows,
4245            uncached_rows,
4246            cached_stats.builds,
4247            cached_stats.hits,
4248            uncached_stats.builds,
4249            cached_transfers.dtoh_calls,
4250            cached_transfers.htod_calls
4251        );
4252
4253        assert_eq!(cached_rows, uncached_rows);
4254        assert_eq!(cached_rows, LEFT_ROWS as u64);
4255        assert_eq!(cached_stats.builds, 1);
4256        assert!(cached_stats.hits >= ITERATIONS as u64);
4257        assert_eq!(uncached_stats.builds, 0);
4258        assert_eq!(cached_transfers.dtoh_calls, 0);
4259        assert_eq!(cached_transfers.htod_calls, 0);
4260        assert_eq!(uncached_transfers.dtoh_calls, 0);
4261        assert_eq!(uncached_transfers.htod_calls, 0);
4262        assert!(
4263            speedup_ratio >= 1.5,
4264            "persistent index speedup {:.3} below 1.5 target",
4265            speedup_ratio
4266        );
4267    }
4268
4269    // ============== MC Relation Reset Tests ==============
4270
4271    #[test]
4272    fn test_reset_for_mc_relations_preserves_static_and_clears_dynamic() {
4273        let mut executor = match create_test_executor() {
4274            Some(e) => e,
4275            None => {
4276                eprintln!("Skipping: no CUDA device");
4277                return;
4278            }
4279        };
4280
4281        executor.register_relation(RelId(1), "base_rel");
4282        executor.register_relation(RelId(2), "dyn_rel");
4283
4284        let schema = Schema::new(vec![("x".to_string(), ScalarType::U32)]);
4285        let base = create_test_buffer(&executor, &[1u32], "x");
4286        let dyn_buf = create_test_buffer(&executor, &[9u32], "x");
4287        executor.put_relation("base_rel", base);
4288        executor.put_relation("dyn_rel", dyn_buf);
4289
4290        executor
4291            .reset_for_mc_relations(&["base_rel"], &[("dyn_rel", schema.clone())])
4292            .unwrap();
4293
4294        assert_eq!(
4295            buffer_row_count(&executor, executor.store().get("base_rel").unwrap()),
4296            1
4297        );
4298        assert_eq!(
4299            buffer_row_count(&executor, executor.store().get("dyn_rel").unwrap()),
4300            0
4301        );
4302    }
4303}