Skip to main content

xlog/
main.rs

1use clap::{Parser, Subcommand, ValueEnum};
2use std::collections::{BTreeSet, HashMap};
3use std::io::Read;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::Duration;
7
8use arrow::csv::WriterBuilder;
9use arrow::util::pretty::pretty_format_batches;
10use xlog_core::{symbol, MemoryBudget, Result, ScalarType, XlogError};
11use xlog_cuda::{CudaKernelProvider, CudaProviderBuilder};
12use xlog_gpu::logic::{
13    download_logic_rows, normalize_program_for_execution, LogicProgram, LogicScalarValue,
14};
15use xlog_ir::{EirBodyLiteral, EirTerm};
16use xlog_logic::ast::{BodyLiteral, ProbEngine, Program};
17use xlog_logic::compile::load_modules;
18#[cfg(feature = "host-io")]
19use xlog_logic::parse_program;
20use xlog_logic::IncrementalParseResult;
21use xlog_logic::{rewrite_magic_sets, MagicSetReport, MagicSetStatus, ParserSession};
22use xlog_logic::{stratify, Compiler};
23use xlog_logic::{QueryProofTrace, RuleProvenance};
24#[cfg(feature = "host-io")]
25use xlog_prob::exact::ExactDdnnfProgram;
26#[cfg(feature = "host-io")]
27use xlog_prob::exact::GpuConfig;
28#[cfg(feature = "host-io")]
29use xlog_prob::mc::{McEvalConfig, McEvalOverrides, McProgram, McSamplingMethod};
30use xlog_prob::provenance::{AggregateLiftReport, Value};
31
32mod generated_rule_diagnostics;
33
34use generated_rule_diagnostics::{
35    explain_generated_rule_diagnostics, print_generated_rule_diagnostics_json,
36    GeneratedRuleDiagnostic,
37};
38
39#[derive(Parser)]
40#[command(author, version, about = "XLOG CLI")]
41pub struct Cli {
42    #[command(subcommand)]
43    command: Command,
44}
45
46#[derive(Subcommand)]
47enum Command {
48    Run(RunArgs),
49    Prob(ProbArgs),
50    Explain(ExplainArgs),
51    Extract(ResolvedProgramArgs),
52    Manifest(ResolvedProgramArgs),
53    Repl(ReplArgs),
54    Watch(WatchArgs),
55}
56
57#[derive(Parser)]
58struct RunArgs {
59    source: PathBuf,
60    #[arg(long, default_value = "0")]
61    device: usize,
62    #[arg(long, default_value = "1024")]
63    memory_mb: u64,
64    #[arg(long)]
65    input: Vec<String>,
66    #[arg(long, value_enum, default_value = "pretty")]
67    output: OutputFormat,
68    /// Export these completed runtime relations with `--output json`.
69    #[arg(long = "materialize-relation")]
70    materialize_relation: Vec<String>,
71    #[arg(long)]
72    output_dir: Option<PathBuf>,
73    /// Show execution statistics (timing, memory usage)
74    #[arg(long)]
75    stats: bool,
76    /// Stats output format (human or json)
77    #[arg(long, value_enum, default_value = "human")]
78    stats_format: StatsFormat,
79    /// Additional directories to search for modules (colon-separated)
80    #[arg(long, value_delimiter = ':')]
81    module_path: Vec<PathBuf>,
82    /// Dump the compiled epistemic execution plan (EIR-derived GPU plan, world-view
83    /// integrity constraints, and fail-closed execution policy) as JSON to this path.
84    /// No-op for ordinary (non-epistemic) programs. This compiled
85    /// epistemic-plan/EIR JSON dump exposes accepted `know`/`possible` literals
86    /// and lets a caller verify that unsupported execution shapes are rejected.
87    #[arg(long)]
88    epistemic_plan_json: Option<PathBuf>,
89    /// Engage the worst-case-optimal join (WCOJ) subsystem for eligible
90    /// multiway rules (triangle + 4-cycle). Without this the deterministic
91    /// runner uses binary joins, which blow up on skewed cyclic queries.
92    /// Sets the documented WCOJ dispatch gates for this process.
93    #[arg(long)]
94    wcoj: bool,
95}
96
97#[derive(Copy, Clone, ValueEnum, Default)]
98enum StatsFormat {
99    #[default]
100    Human,
101    Json,
102}
103
104#[derive(Parser)]
105struct ProbArgs {
106    source: PathBuf,
107    #[arg(long, default_value = "0")]
108    device: usize,
109    #[arg(long, default_value = "1024")]
110    memory_mb: u64,
111    #[arg(long, value_enum)]
112    prob_engine: Option<ProbEngineCli>,
113    #[arg(long)]
114    samples: Option<usize>,
115    #[arg(long)]
116    seed: Option<u64>,
117    #[arg(long)]
118    confidence: Option<f64>,
119    #[arg(long, value_enum)]
120    prob_method: Option<ProbMethodCli>,
121    #[arg(long, alias = "max-nonmonotone-iterations")]
122    prob_max_nonmonotone_iterations: Option<usize>,
123    /// Allow the labeled CPU oracle when the resident GPU MC engine rejects
124    /// the program (negation, aggregates, ...). Fail-closed when unset; the
125    /// result is labeled `mc_engine: cpu-oracle` and is not GPU-native evidence.
126    #[arg(long)]
127    allow_cpu_oracle: bool,
128    #[arg(long, value_enum, default_value = "pretty")]
129    output: ProbOutputFormat,
130    #[arg(long)]
131    output_dir: Option<PathBuf>,
132    /// Additional directories to search for modules (colon-separated)
133    #[arg(long, value_delimiter = ':')]
134    module_path: Vec<PathBuf>,
135}
136
137#[derive(Parser)]
138struct ExplainArgs {
139    source: PathBuf,
140    #[arg(long, value_enum, default_value = "text")]
141    format: ExplainFormat,
142    /// Additional directories to search for modules (colon-separated)
143    #[arg(long, value_delimiter = ':')]
144    module_path: Vec<PathBuf>,
145}
146
147#[derive(Parser)]
148struct ResolvedProgramArgs {
149    source: PathBuf,
150    /// Root used to emit portable source-relative module paths
151    #[arg(long)]
152    source_root: PathBuf,
153    /// Additional directories to search for modules (colon-separated)
154    #[arg(long, value_delimiter = ':')]
155    module_path: Vec<PathBuf>,
156}
157
158#[derive(Parser)]
159struct ReplArgs {
160    /// Additional directories to search for modules (colon-separated)
161    #[arg(long, value_delimiter = ':')]
162    module_path: Vec<PathBuf>,
163}
164
165#[derive(Parser)]
166struct WatchArgs {
167    source: PathBuf,
168    #[arg(long, default_value = "250")]
169    debounce_ms: u64,
170    #[arg(long)]
171    explain: bool,
172    #[arg(long)]
173    once: bool,
174    /// Additional directories to search for modules (colon-separated)
175    #[arg(long, value_delimiter = ':')]
176    module_path: Vec<PathBuf>,
177}
178
179#[derive(Copy, Clone, ValueEnum)]
180enum ExplainFormat {
181    Text,
182    Json,
183    Dot,
184}
185
186#[derive(Copy, Clone, ValueEnum)]
187enum OutputFormat {
188    Pretty,
189    Csv,
190    Arrow,
191    Json,
192}
193
194#[derive(Copy, Clone, ValueEnum)]
195enum ProbOutputFormat {
196    Pretty,
197    Csv,
198    Arrow,
199    Json,
200}
201
202#[derive(Copy, Clone, ValueEnum)]
203enum ProbEngineCli {
204    #[value(name = "exact_ddnnf")]
205    ExactDdnnf,
206    Mc,
207}
208
209#[derive(Copy, Clone, ValueEnum)]
210enum ProbMethodCli {
211    Rejection,
212    #[value(name = "evidence_clamping")]
213    EvidenceClamping,
214}
215
216fn main() -> Result<()> {
217    let cli = Cli::parse();
218    match cli.command {
219        Command::Run(args) => run_deterministic(args),
220        Command::Prob(args) => run_probabilistic(args),
221        Command::Explain(args) => explain(args),
222        Command::Extract(args) => extract(args),
223        Command::Manifest(args) => manifest(args),
224        Command::Repl(args) => repl(args),
225        Command::Watch(args) => watch(args),
226    }
227}
228
229fn extract(args: ResolvedProgramArgs) -> Result<()> {
230    let resolver = load_modules(&args.source, args.module_path)
231        .map_err(|error| XlogError::Execution(format!("Module resolution failed: {error}")))?;
232    let extraction = resolver
233        .resolved_program_extraction(&args.source_root)
234        .map_err(|error| XlogError::Execution(format!("Program extraction failed: {error}")))?;
235    let json = serde_json::to_string_pretty(&extraction).map_err(|error| {
236        XlogError::Execution(format!("Extraction serialization failed: {error}"))
237    })?;
238    println!("{json}");
239    Ok(())
240}
241
242fn manifest(args: ResolvedProgramArgs) -> Result<()> {
243    let resolver = load_modules(&args.source, args.module_path)
244        .map_err(|error| XlogError::Execution(format!("Module resolution failed: {error}")))?;
245    let manifest = resolver
246        .resolved_program_manifest(&args.source_root)
247        .map_err(|error| XlogError::Execution(format!("Manifest construction failed: {error}")))?;
248    let json = serde_json::to_string_pretty(&manifest)
249        .map_err(|error| XlogError::Execution(format!("Manifest serialization failed: {error}")))?;
250    println!("{json}");
251    Ok(())
252}
253
254fn explain(args: ExplainArgs) -> Result<()> {
255    let source = std::fs::read_to_string(&args.source).map_err(|e| {
256        XlogError::Execution(format!("Failed to read {}: {}", args.source.display(), e))
257    })?;
258    let mut parser_session = ParserSession::new();
259    let parsed = parser_session.parse_path(&args.source, &source)?;
260    let parsed = resolve_explain_imports(parsed, &args.source, args.module_path)?;
261    let report = build_explain_report(parsed, Some(&args.source))?;
262    match args.format {
263        ExplainFormat::Text => print_explain_text(&report),
264        ExplainFormat::Json => print_explain_json(&report),
265        ExplainFormat::Dot => print_magic_dot(&report.magic_sets),
266    }
267    Ok(())
268}
269
270fn resolve_explain_imports(
271    mut parsed: IncrementalParseResult,
272    source_path: &Path,
273    module_path: Vec<PathBuf>,
274) -> Result<IncrementalParseResult> {
275    parsed.program = resolve_program_imports(
276        parsed.program,
277        source_path,
278        module_path,
279        ModuleMergeErrorKind::Execution,
280    )?;
281    Ok(parsed)
282}
283
284#[derive(Clone, Copy)]
285enum ModuleMergeErrorKind {
286    Execution,
287    Compilation,
288}
289
290fn resolve_program_imports(
291    program: Program,
292    source_path: &Path,
293    module_path: Vec<PathBuf>,
294    merge_error_kind: ModuleMergeErrorKind,
295) -> Result<Program> {
296    if program.imports.is_empty() {
297        return Ok(program);
298    }
299    let resolver = load_modules(source_path, module_path)
300        .map_err(|e| XlogError::Execution(format!("Module resolution failed: {}", e)))?;
301    warn_ignored_import_pragmas(&resolver);
302    resolver
303        .merge_imports(program)
304        .map_err(|error| match merge_error_kind {
305            ModuleMergeErrorKind::Execution => {
306                XlogError::Execution(format!("Module resolution failed: {error}"))
307            }
308            ModuleMergeErrorKind::Compilation => {
309                XlogError::Compilation(format!("Module resolution failed: {error}"))
310            }
311        })
312}
313
314/// Surface pragmas declared in imported modules on stderr. Pragmas are
315/// entry-file-scoped, so these directives are dropped at merge time; the
316/// warning keeps that scoping from being silent.
317fn warn_ignored_import_pragmas(resolver: &xlog_logic::resolver::ModuleResolver) {
318    for warning in resolver.ignored_import_pragmas() {
319        eprintln!("{}", warning);
320    }
321}
322
323fn repl(args: ReplArgs) -> Result<()> {
324    let _ = args.module_path;
325    let mut input = String::new();
326    std::io::stdin()
327        .read_to_string(&mut input)
328        .map_err(|e| XlogError::Execution(format!("Failed to read stdin: {}", e)))?;
329    let mut parser_session = ParserSession::new();
330    let parsed = parser_session.parse_path("<repl>", &input)?;
331    println!(
332        "repl: statements={} cache_hits={} cache_misses={}",
333        parsed.stats.statement_count, parsed.stats.hits, parsed.stats.misses
334    );
335    println!(
336        "state: rules={} queries={} prob_queries={}",
337        parsed.program.rules.len(),
338        parsed.program.queries.len(),
339        parsed.program.prob_queries.len()
340    );
341    Ok(())
342}
343
344fn watch(args: WatchArgs) -> Result<()> {
345    let mut parser_session = ParserSession::new();
346    loop {
347        let source = std::fs::read_to_string(&args.source).map_err(|e| {
348            XlogError::Execution(format!("Failed to read {}: {}", args.source.display(), e))
349        })?;
350        let parsed = parser_session.parse_path(&args.source, &source)?;
351        let parsed = if args.explain {
352            resolve_explain_imports(parsed, &args.source, args.module_path.clone())?
353        } else {
354            parsed
355        };
356        println!(
357            "watch: statements={} cache_hits={} cache_misses={}",
358            parsed.stats.statement_count, parsed.stats.hits, parsed.stats.misses
359        );
360        if args.explain {
361            let report = build_explain_report(parsed, Some(&args.source))?;
362            print_explain_text(&report);
363        }
364        if args.once {
365            break;
366        }
367        std::thread::sleep(Duration::from_millis(args.debounce_ms));
368    }
369    Ok(())
370}
371
372struct ExplainReport {
373    program: Program,
374    parse_stats: xlog_logic::ParseCacheStats,
375    epistemic: serde_json::Value,
376    magic_sets: MagicSetReport,
377    aggregate_lifting: Vec<AggregateLiftReport>,
378    generated_rule_diagnostics: Vec<GeneratedRuleDiagnostic>,
379    generated_rule_diagnostics_status: String,
380    generated_rule_diagnostics_reason: Option<String>,
381    rule_provenance: Vec<RuleProvenance>,
382    proof_traces: Vec<QueryProofTrace>,
383    stratification_status: String,
384    stratification_reason: Option<String>,
385    stratification_count: usize,
386    aggregate_lifting_status: String,
387    aggregate_lifting_reason: Option<String>,
388    rir_status: String,
389    rir_reason: Option<String>,
390    rir_sccs: usize,
391    optimizer_status: String,
392    optimizer_reason: Option<String>,
393    optimizer_memory_peak: u64,
394    wcoj_status: String,
395    wcoj_reason: Option<String>,
396}
397
398fn build_explain_report(
399    parsed: xlog_logic::IncrementalParseResult,
400    source_path: Option<&Path>,
401) -> Result<ExplainReport> {
402    let source_program = parsed.program;
403    match normalize_program_for_execution(source_program.clone()) {
404        Ok(analysis_program) => {
405            let magic_rewrite = rewrite_magic_sets(&analysis_program)?;
406            let (rule_provenance, proof_traces) = explain_source_diagnostics(
407                &source_program,
408                &analysis_program,
409                &magic_rewrite.program,
410            );
411            let aggregate_lifting = explain_aggregate_lifting(&analysis_program)?;
412            let epistemic = explain_epistemic(&analysis_program);
413            let (stratification_status, stratification_count) = match stratify(&analysis_program) {
414                Ok(strata) => ("ok".to_string(), strata.len()),
415                Err(error) => (format!("error: {error}"), 0),
416            };
417            let mut compiler = Compiler::new();
418            let (
419                rir_status,
420                rir_reason,
421                rir_sccs,
422                optimizer_status,
423                optimizer_reason,
424                optimizer_memory_peak,
425            ) = match compiler.compile_program(&analysis_program) {
426                Ok(plan) => (
427                    "ok".to_string(),
428                    None,
429                    plan.sccs.len(),
430                    "ok".to_string(),
431                    None,
432                    plan.est_memory_peak,
433                ),
434                Err(error) => {
435                    let reason = format!("RIR compilation failed: {error}");
436                    (
437                        format!("error: {error}"),
438                        Some(reason.clone()),
439                        0,
440                        "not_available".to_string(),
441                        Some(reason),
442                        0,
443                    )
444                }
445            };
446            let (
447                generated_rule_diagnostics,
448                generated_rule_diagnostics_status,
449                generated_rule_diagnostics_reason,
450            ) = if let Some(reason) = &rir_reason {
451                (
452                    Vec::new(),
453                    "not_available".to_string(),
454                    Some(reason.clone()),
455                )
456            } else {
457                match explain_generated_rule_diagnostics(
458                    &source_program,
459                    &analysis_program,
460                    source_path,
461                ) {
462                    Ok(diagnostics) => (diagnostics, "ok".to_string(), None),
463                    Err(error) => {
464                        let reason = format!("generated-rule diagnostics failed: {error}");
465                        (Vec::new(), "not_available".to_string(), Some(reason))
466                    }
467                }
468            };
469            let (wcoj_status, wcoj_reason) = if let Some(reason) = &rir_reason {
470                ("not_available".to_string(), Some(reason.clone()))
471            } else {
472                ("reported".to_string(), None)
473            };
474            Ok(ExplainReport {
475                program: source_program,
476                parse_stats: parsed.stats,
477                epistemic,
478                magic_sets: magic_rewrite.report,
479                aggregate_lifting,
480                generated_rule_diagnostics,
481                generated_rule_diagnostics_status,
482                generated_rule_diagnostics_reason,
483                rule_provenance,
484                proof_traces,
485                stratification_status,
486                stratification_reason: None,
487                stratification_count,
488                aggregate_lifting_status: "ok".to_string(),
489                aggregate_lifting_reason: None,
490                rir_status,
491                rir_reason,
492                rir_sccs,
493                optimizer_status,
494                optimizer_reason,
495                optimizer_memory_peak,
496                wcoj_status,
497                wcoj_reason,
498            })
499        }
500        Err(error) => {
501            let normalization_reason = format!("execution normalization failed: {error}");
502            let rule_provenance = xlog_logic::rule_provenance(&source_program, None);
503            let proof_traces = xlog_logic::query_proof_traces(&source_program, &rule_provenance);
504            Ok(ExplainReport {
505                program: source_program,
506                parse_stats: parsed.stats,
507                epistemic: unavailable_epistemic_analysis(&normalization_reason),
508                magic_sets: MagicSetReport {
509                    status: MagicSetStatus::Declined,
510                    generated_predicates: Vec::new(),
511                    adorned_predicates: Vec::new(),
512                    declined_reasons: vec![normalization_reason.clone()],
513                },
514                aggregate_lifting: Vec::new(),
515                generated_rule_diagnostics: Vec::new(),
516                generated_rule_diagnostics_status: "not_available".to_string(),
517                generated_rule_diagnostics_reason: Some(normalization_reason.clone()),
518                rule_provenance,
519                proof_traces,
520                stratification_status: "not_available".to_string(),
521                stratification_reason: Some(normalization_reason.clone()),
522                stratification_count: 0,
523                aggregate_lifting_status: "not_available".to_string(),
524                aggregate_lifting_reason: Some(normalization_reason.clone()),
525                rir_status: "not_available".to_string(),
526                rir_reason: Some(normalization_reason.clone()),
527                rir_sccs: 0,
528                optimizer_status: "not_available".to_string(),
529                optimizer_reason: Some(normalization_reason.clone()),
530                optimizer_memory_peak: 0,
531                wcoj_status: "not_available".to_string(),
532                wcoj_reason: Some(normalization_reason),
533            })
534        }
535    }
536}
537
538fn unavailable_epistemic_analysis(reason: &str) -> serde_json::Value {
539    let unavailable = serde_json::json!({
540        "status": "not_available",
541        "reason": reason,
542    });
543    serde_json::json!({
544        "eir": unavailable.clone(),
545        "gpu_plan": unavailable.clone(),
546        "executable_plan": unavailable,
547    })
548}
549
550fn explain_source_diagnostics(
551    source_program: &Program,
552    analysis_program: &Program,
553    rewritten_program: &Program,
554) -> (Vec<RuleProvenance>, Vec<QueryProofTrace>) {
555    xlog_logic::source_diagnostics(source_program, analysis_program, Some(rewritten_program))
556}
557
558fn explain_epistemic(program: &Program) -> serde_json::Value {
559    if !program_has_epistemic_literals(program) {
560        let not_applicable = serde_json::json!({
561            "status": "not_applicable",
562            "reason": "program has no epistemic literals",
563            "epistemic_literal_count": 0,
564        });
565        return serde_json::json!({
566            "eir": not_applicable.clone(),
567            "gpu_plan": not_applicable.clone(),
568            "executable_plan": not_applicable,
569        });
570    }
571
572    let eir = match xlog_logic::build_eir(program) {
573        Ok(eir) => {
574            let literals = eir
575                .rules
576                .iter()
577                .enumerate()
578                .flat_map(|(rule_index, rule)| {
579                    rule.body.iter().filter_map(move |lit| match lit {
580                        EirBodyLiteral::Epistemic(epistemic) => Some(serde_json::json!({
581                            "rule_index": rule_index,
582                            "literal": eir_epistemic_literal_json(epistemic),
583                        })),
584                        _ => None,
585                    })
586                })
587                .collect::<Vec<_>>();
588            let rule_summaries = eir
589                .rules
590                .iter()
591                .enumerate()
592                .map(|(rule_index, rule)| {
593                    let epistemic_literal_count = rule
594                        .body
595                        .iter()
596                        .filter(|lit| matches!(lit, EirBodyLiteral::Epistemic(_)))
597                        .count();
598                    let relational_body_atoms = rule
599                        .body
600                        .iter()
601                        .filter(|lit| {
602                            matches!(lit, EirBodyLiteral::Relational { negated: false, .. })
603                        })
604                        .count();
605                    serde_json::json!({
606                        "rule_index": rule_index,
607                        "head": eir_atom_json(&rule.head),
608                        "body_literal_count": rule.body.len(),
609                        "epistemic_literal_count": epistemic_literal_count,
610                        "relational_body_atoms": relational_body_atoms,
611                    })
612                })
613                .collect::<Vec<_>>();
614            serde_json::json!({
615                "status": "ok",
616                "mode": format!("{:?}", eir.mode),
617                "rule_count": eir.rules.len(),
618                "epistemic_literal_count": literals.len(),
619                "literals": literals,
620                "rules": rule_summaries,
621            })
622        }
623        Err(err) => serde_json::json!({
624            "status": "error",
625            "error": err.to_string(),
626        }),
627    };
628
629    let gpu_plan = match xlog_logic::epistemic::plan_epistemic_gpu_execution(program) {
630        Ok(plan) => {
631            let reductions = plan
632                .reductions
633                .iter()
634                .map(|reduction| {
635                    serde_json::json!({
636                        "rule_index": reduction.rule_index,
637                        "head_predicate": &reduction.head_predicate,
638                        "relational_body_atoms": reduction.relational_body_atoms,
639                        "wcoj_status": format!("{:?}", reduction.wcoj_status),
640                    })
641                })
642                .collect::<Vec<_>>();
643            let tuple_membership = plan
644                .tuple_membership_bindings
645                .iter()
646                .map(|binding| {
647                    serde_json::json!({
648                        "literal_index": binding.literal_index,
649                        "reduction_index": binding.reduction_index,
650                        "predicate": &binding.predicate,
651                        "arity": binding.arity,
652                        "key_columns": binding.key_columns,
653                        "key_terms": binding.key_terms.iter().map(eir_term_label).collect::<Vec<_>>(),
654                        "bound_output_columns": binding.bound_output_columns,
655                        "op": format!("{:?}", binding.op),
656                        "negated": binding.negated,
657                    })
658                })
659                .collect::<Vec<_>>();
660            serde_json::json!({
661                "status": "ok",
662                "mode": format!("{:?}", plan.mode),
663                "epistemic_literal_count": plan.epistemic_literals.len(),
664                "required_phases": plan.required_phases.iter().map(|phase| format!("{:?}", phase)).collect::<Vec<_>>(),
665                "required_kernel_phases": plan.required_kernel_phases.iter().map(|phase| format!("{:?}", phase)).collect::<Vec<_>>(),
666                "required_buffers": plan.required_buffers.iter().map(|buffer| format!("{:?}", buffer)).collect::<Vec<_>>(),
667                "reductions": reductions,
668                "tuple_membership_bindings": tuple_membership,
669                "solver_contract": {
670                    "assumption_count": plan.solver_contract.assumption_bindings.len(),
671                    "required_capabilities": plan.solver_contract.required_capabilities.iter().map(|cap| format!("{:?}", cap)).collect::<Vec<_>>(),
672                    "required_statuses": plan.solver_contract.required_statuses.iter().map(|status| format!("{:?}", status)).collect::<Vec<_>>(),
673                },
674                "execution_backend": epistemic_execution_backend_label(plan.execution_backend),
675                "fallback_policy": epistemic_fallback_policy_label(plan.fallback_policy),
676            })
677        }
678        Err(err) => serde_json::json!({
679            "status": "error",
680            "error": err.to_string(),
681        }),
682    };
683
684    let executable_plan = match xlog_logic::epistemic::compile_epistemic_gpu_execution(program) {
685        Ok(plan) => serde_json::json!({
686            "status": "ok",
687            "relation_id_count": plan.relation_ids.len(),
688            "reduced_runtime_sccs": plan.reduced_runtime_plan.sccs.len(),
689            "reduced_runtime_est_memory_peak": plan.reduced_runtime_plan.est_memory_peak,
690            "gpu_plan_literal_count": plan.gpu_plan.epistemic_literals.len(),
691            "execution_backend": epistemic_execution_backend_label(plan.gpu_plan.execution_backend),
692            "fallback_policy": epistemic_fallback_policy_label(plan.gpu_plan.fallback_policy),
693        }),
694        Err(err) => serde_json::json!({
695            "status": "error",
696            "error": err.to_string(),
697        }),
698    };
699
700    serde_json::json!({
701        "eir": eir,
702        "gpu_plan": gpu_plan,
703        "executable_plan": executable_plan,
704    })
705}
706
707fn program_has_epistemic_literals(program: &Program) -> bool {
708    program.rules.iter().any(|rule| {
709        rule.body
710            .iter()
711            .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
712    }) || program.constraints.iter().any(|constraint| {
713        constraint
714            .body
715            .iter()
716            .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
717    })
718}
719
720fn eir_atom_json(atom: &xlog_ir::EirAtom) -> serde_json::Value {
721    serde_json::json!({
722        "predicate": &atom.predicate,
723        "arity": atom.arity,
724        "terms": atom.terms.iter().map(eir_term_label).collect::<Vec<_>>(),
725    })
726}
727
728fn eir_epistemic_literal_json(lit: &xlog_ir::EirEpistemicLiteral) -> serde_json::Value {
729    serde_json::json!({
730        "op": format!("{:?}", lit.op),
731        "negated": lit.negated,
732        "atom": eir_atom_json(&lit.atom),
733    })
734}
735
736fn epistemic_execution_backend_label(backend: xlog_ir::EpistemicExecutionBackend) -> &'static str {
737    match backend {
738        xlog_ir::EpistemicExecutionBackend::Gpu => "gpu",
739    }
740}
741
742fn epistemic_fallback_policy_label(policy: xlog_ir::EpistemicFallbackPolicy) -> &'static str {
743    match policy {
744        xlog_ir::EpistemicFallbackPolicy::RejectUnsupported => "reject_unsupported",
745    }
746}
747
748fn eir_term_label(term: &EirTerm) -> String {
749    match term {
750        EirTerm::Variable(name) => name.clone(),
751        EirTerm::Anonymous => "_".to_string(),
752        EirTerm::Integer(value) => value.to_string(),
753        EirTerm::FloatBits(bits) => f64::from_bits(*bits).to_string(),
754        EirTerm::String(value) => value.clone(),
755        EirTerm::Symbol(id) => symbol::resolve(*id),
756        EirTerm::List(items) => format!(
757            "[{}]",
758            items
759                .iter()
760                .map(eir_term_label)
761                .collect::<Vec<_>>()
762                .join(", ")
763        ),
764        EirTerm::Cons { head, tail } => {
765            format!("{}|{}", eir_term_label(head), eir_term_label(tail))
766        }
767        EirTerm::Compound { functor, args } => format!(
768            "{}({})",
769            functor,
770            args.iter()
771                .map(eir_term_label)
772                .collect::<Vec<_>>()
773                .join(", ")
774        ),
775        EirTerm::PredRef(name) => name.clone(),
776        EirTerm::Aggregate { op, variable } => format!("{}({})", op, variable),
777    }
778}
779
780fn explain_aggregate_lifting(program: &Program) -> Result<Vec<AggregateLiftReport>> {
781    let has_probabilistic_source =
782        !program.prob_facts.is_empty() || !program.annotated_disjunctions.is_empty();
783    let has_aggregate_rule = program.proper_rules().any(|rule| rule.has_aggregation());
784    if !(has_probabilistic_source && has_aggregate_rule) {
785        return Ok(Vec::new());
786    }
787    Ok(xlog_prob::provenance::extract_from_program(program)?.aggregate_lifting)
788}
789
790fn print_explain_text(report: &ExplainReport) {
791    println!("parse:");
792    println!("  statements: {}", report.parse_stats.statement_count);
793    println!("ast:");
794    println!("  rules: {}", report.program.rules.len());
795    println!("  queries: {}", report.program.queries.len());
796    println!("stratification:");
797    println!("  status: {}", report.stratification_status);
798    if let Some(reason) = &report.stratification_reason {
799        println!("  reason: {reason}");
800    }
801    println!("  strata: {}", report.stratification_count);
802    println!("rir:");
803    println!("  status: {}", report.rir_status);
804    if let Some(reason) = &report.rir_reason {
805        println!("  reason: {reason}");
806    }
807    println!("  sccs: {}", report.rir_sccs);
808    println!("optimizer:");
809    println!("  status: {}", report.optimizer_status);
810    if let Some(reason) = &report.optimizer_reason {
811        println!("  reason: {reason}");
812    }
813    println!("  est_memory_peak: {}", report.optimizer_memory_peak);
814    println!("wcoj:");
815    println!("  status: {}", report.wcoj_status);
816    if let Some(reason) = &report.wcoj_reason {
817        println!("  reason: {reason}");
818    }
819    println!("epistemic:");
820    for section in ["eir", "gpu_plan", "executable_plan"] {
821        let analysis = report.epistemic.get(section);
822        let status = analysis
823            .and_then(|value| value.get("status"))
824            .and_then(serde_json::Value::as_str)
825            .unwrap_or("not_available");
826        println!("  {section}: {status}");
827        if let Some(reason) = analysis
828            .and_then(|value| value.get("reason"))
829            .and_then(serde_json::Value::as_str)
830        {
831            println!("  {section}_reason: {reason}");
832        }
833    }
834    print_magic_text(&report.magic_sets);
835    if report.aggregate_lifting_status != "ok" || !report.aggregate_lifting.is_empty() {
836        println!("aggregate_lifting:");
837        println!("  status: {}", report.aggregate_lifting_status);
838        if let Some(reason) = &report.aggregate_lifting_reason {
839            println!("  reason: {reason}");
840        }
841        for entry in &report.aggregate_lifting {
842            println!(
843                "  - predicate: {} operator: {} status: {} domain: {} uncertain: {} cap: {}",
844                entry.predicate,
845                entry.operator,
846                entry.status.as_str(),
847                entry.domain_size,
848                entry.uncertain_rows,
849                entry.cap
850            );
851        }
852    }
853    if !report.rule_provenance.is_empty() {
854        println!("rule_provenance:");
855        for entry in &report.rule_provenance {
856            println!(
857                "  - id: {} source_kind: {} head: {}",
858                entry.rule_id,
859                entry.source_kind.as_str(),
860                entry.head
861            );
862        }
863    }
864    if !report.proof_traces.is_empty() {
865        println!("proof_traces:");
866        for entry in &report.proof_traces {
867            println!(
868                "  - query: {} rules: {} source_facts: {}",
869                entry.query,
870                entry.rule_ids.len(),
871                entry.source_facts.len()
872            );
873        }
874    }
875    println!("generated_rule_diagnostics:");
876    println!("  status: {}", report.generated_rule_diagnostics_status);
877    if let Some(reason) = &report.generated_rule_diagnostics_reason {
878        println!("  reason: {reason}");
879    }
880    println!("  rules: {}", report.generated_rule_diagnostics.len());
881}
882
883fn print_magic_text(report: &MagicSetReport) {
884    println!("magic_sets:");
885    println!("  status: {}", magic_status_label(report.status));
886    if !report.adorned_predicates.is_empty() {
887        println!("  adorned_predicates:");
888        for pred in &report.adorned_predicates {
889            println!("    - {}", pred);
890        }
891    }
892    if !report.generated_predicates.is_empty() {
893        println!("  generated_predicates:");
894        for pred in &report.generated_predicates {
895            println!("    - {}", pred);
896        }
897    }
898    if !report.declined_reasons.is_empty() {
899        println!("  declined_reasons:");
900        for reason in &report.declined_reasons {
901            println!("    - {}", reason);
902        }
903    }
904}
905
906fn print_explain_json(report: &ExplainReport) {
907    println!("{{");
908    println!("  \"parse\": {{");
909    println!(
910        "    \"statements\": {},",
911        report.parse_stats.statement_count
912    );
913    println!("    \"cache_hits\": {},", report.parse_stats.hits);
914    println!("    \"cache_misses\": {}", report.parse_stats.misses);
915    println!("  }},");
916    println!("  \"ast\": {{");
917    println!("    \"rules\": {},", report.program.rules.len());
918    println!("    \"queries\": {},", report.program.queries.len());
919    println!(
920        "    \"prob_queries\": {}",
921        report.program.prob_queries.len()
922    );
923    println!("  }},");
924    println!("  \"stratification\": {{");
925    println!(
926        "    \"status\": \"{}\",",
927        json_escape(&report.stratification_status)
928    );
929    println!(
930        "    \"reason\": {},",
931        json_optional_string(report.stratification_reason.as_deref())
932    );
933    println!("    \"strata\": {}", report.stratification_count);
934    println!("  }},");
935    println!("  \"rir\": {{");
936    println!("    \"status\": \"{}\",", json_escape(&report.rir_status));
937    println!(
938        "    \"reason\": {},",
939        json_optional_string(report.rir_reason.as_deref())
940    );
941    println!("    \"sccs\": {}", report.rir_sccs);
942    println!("  }},");
943    println!("  \"optimizer\": {{");
944    println!(
945        "    \"status\": \"{}\",",
946        json_escape(&report.optimizer_status)
947    );
948    println!(
949        "    \"reason\": {},",
950        json_optional_string(report.optimizer_reason.as_deref())
951    );
952    println!("    \"est_memory_peak\": {}", report.optimizer_memory_peak);
953    println!("  }},");
954    println!("  \"wcoj\": {{");
955    println!("    \"status\": \"{}\",", json_escape(&report.wcoj_status));
956    println!(
957        "    \"reason\": {}",
958        json_optional_string(report.wcoj_reason.as_deref())
959    );
960    println!("  }},");
961    println!("  \"epistemic\": {},", report.epistemic);
962    println!("  \"magic_sets\": {{");
963    println!(
964        "    \"status\": \"{}\",",
965        json_escape(magic_status_label(report.magic_sets.status))
966    );
967    println!(
968        "    \"adorned_predicates\": {},",
969        json_string_array(&report.magic_sets.adorned_predicates)
970    );
971    println!(
972        "    \"generated_predicates\": {},",
973        json_string_array(&report.magic_sets.generated_predicates)
974    );
975    println!(
976        "    \"declined_reasons\": {}",
977        json_string_array(&report.magic_sets.declined_reasons)
978    );
979    println!("  }},");
980    println!("  \"probability\": {{");
981    println!(
982        "    \"engine\": \"{}\",",
983        match report.program.prob_engine() {
984            ProbEngine::ExactDdnnf => "exact_ddnnf",
985            ProbEngine::Mc => "mc",
986        }
987    );
988    println!(
989        "    \"aggregate_lifting_status\": \"{}\",",
990        json_escape(&report.aggregate_lifting_status)
991    );
992    println!(
993        "    \"aggregate_lifting_reason\": {},",
994        json_optional_string(report.aggregate_lifting_reason.as_deref())
995    );
996    println!(
997        "    \"aggregate_lifting_count\": {}",
998        report.aggregate_lifting.len()
999    );
1000    println!("  }},");
1001    println!("  \"aggregate_lifting\": [");
1002    for (idx, entry) in report.aggregate_lifting.iter().enumerate() {
1003        let suffix = if idx + 1 == report.aggregate_lifting.len() {
1004            ""
1005        } else {
1006            ","
1007        };
1008        println!("    {{");
1009        println!(
1010            "      \"predicate\": \"{}\",",
1011            json_escape(&entry.predicate)
1012        );
1013        println!(
1014            "      \"group_key\": {},",
1015            json_value_array(&entry.group_key)
1016        );
1017        println!("      \"operator\": \"{}\",", json_escape(&entry.operator));
1018        println!(
1019            "      \"finite_domain_source\": \"{}\",",
1020            json_escape(&entry.finite_domain_source)
1021        );
1022        println!(
1023            "      \"deterministic_rows\": {},",
1024            entry.deterministic_rows
1025        );
1026        println!("      \"uncertain_rows\": {},", entry.uncertain_rows);
1027        println!("      \"domain_size\": {},", entry.domain_size);
1028        println!("      \"cap\": {},", entry.cap);
1029        println!("      \"status\": \"{}\",", entry.status.as_str());
1030        println!("      \"reason\": \"{}\",", json_escape(&entry.reason));
1031        println!("      \"naive_outcomes\": {},", entry.naive_outcomes);
1032        println!(
1033            "      \"dynamic_programming_states\": {}",
1034            entry.dynamic_programming_states
1035        );
1036        println!("    }}{}", suffix);
1037    }
1038    println!("  ],");
1039    print_rule_provenance_json(&report.rule_provenance);
1040    println!(",");
1041    print_proof_traces_json(&report.proof_traces);
1042    println!(",");
1043    println!(
1044        "  \"generated_rule_diagnostics_status\": \"{}\",",
1045        json_escape(&report.generated_rule_diagnostics_status)
1046    );
1047    println!(
1048        "  \"generated_rule_diagnostics_reason\": {},",
1049        json_optional_string(report.generated_rule_diagnostics_reason.as_deref())
1050    );
1051    print_generated_rule_diagnostics_json(&report.generated_rule_diagnostics);
1052    println!("}}");
1053}
1054
1055fn print_rule_provenance_json(entries: &[RuleProvenance]) {
1056    println!("  \"rule_provenance\": [");
1057    for (idx, entry) in entries.iter().enumerate() {
1058        let suffix = if idx + 1 == entries.len() { "" } else { "," };
1059        println!("    {{");
1060        println!("      \"rule_id\": \"{}\",", json_escape(&entry.rule_id));
1061        println!("      \"head\": \"{}\",", json_escape(&entry.head));
1062        println!(
1063            "      \"source_kind\": \"{}\",",
1064            json_escape(entry.source_kind.as_str())
1065        );
1066        println!(
1067            "      \"source_span\": {},",
1068            json_optional_string(entry.source_span.as_deref())
1069        );
1070        println!(
1071            "      \"generation_trace_hash\": {},",
1072            json_optional_string(entry.generation_trace_hash.as_deref())
1073        );
1074        println!(
1075            "      \"support_relation_ids\": {},",
1076            json_string_array(&entry.support_relation_ids)
1077        );
1078        println!(
1079            "      \"counterexample_relation_ids\": {}",
1080            json_string_array(&entry.counterexample_relation_ids)
1081        );
1082        println!("    }}{}", suffix);
1083    }
1084    println!("  ]");
1085}
1086
1087fn print_proof_traces_json(entries: &[QueryProofTrace]) {
1088    println!("  \"proof_traces\": [");
1089    for (idx, entry) in entries.iter().enumerate() {
1090        let suffix = if idx + 1 == entries.len() { "" } else { "," };
1091        println!("    {{");
1092        println!("      \"query_id\": \"{}\",", json_escape(&entry.query_id));
1093        println!("      \"query\": \"{}\",", json_escape(&entry.query));
1094        println!(
1095            "      \"answer_relation\": \"{}\",",
1096            json_escape(&entry.answer_relation)
1097        );
1098        println!(
1099            "      \"rule_ids\": {},",
1100            json_string_array(&entry.rule_ids)
1101        );
1102        println!(
1103            "      \"source_facts\": {},",
1104            json_string_array(&entry.source_facts)
1105        );
1106        println!(
1107            "      \"rejected_alternatives\": {}",
1108            json_string_array(&entry.rejected_alternatives)
1109        );
1110        println!("    }}{}", suffix);
1111    }
1112    println!("  ]");
1113}
1114
1115fn print_magic_dot(report: &MagicSetReport) {
1116    println!("digraph xlog_magic_sets {{");
1117    println!(
1118        "  status [label=\"status: {}\"];",
1119        magic_status_label(report.status)
1120    );
1121    for pred in &report.generated_predicates {
1122        println!("  \"{}\" [shape=box];", dot_escape(pred));
1123    }
1124    for pred in &report.adorned_predicates {
1125        println!("  \"{}\" [shape=ellipse];", dot_escape(pred));
1126    }
1127    for (index, reason) in report.declined_reasons.iter().enumerate() {
1128        println!(
1129            "  reason_{index} [shape=note,label=\"reason: {}\"];",
1130            dot_escape(reason)
1131        );
1132        println!("  status -> reason_{index} [style=dashed];");
1133    }
1134    println!("}}");
1135}
1136
1137fn magic_status_label(status: MagicSetStatus) -> &'static str {
1138    match status {
1139        MagicSetStatus::Disabled => "disabled",
1140        MagicSetStatus::Applied => "applied",
1141        MagicSetStatus::Declined => "declined",
1142    }
1143}
1144
1145fn json_string_array(items: &[String]) -> String {
1146    let values = items
1147        .iter()
1148        .map(|item| format!("\"{}\"", json_escape(item)))
1149        .collect::<Vec<_>>()
1150        .join(", ");
1151    format!("[{}]", values)
1152}
1153
1154fn json_value_array(items: &[Value]) -> String {
1155    let values = items.iter().map(json_value).collect::<Vec<_>>().join(", ");
1156    format!("[{}]", values)
1157}
1158
1159fn json_value(value: &Value) -> String {
1160    match value {
1161        Value::I64(v) => v.to_string(),
1162        Value::F64(bits) => {
1163            let v = f64::from_bits(*bits);
1164            if v.is_finite() {
1165                v.to_string()
1166            } else {
1167                format!("\"{}\"", json_escape(&v.to_string()))
1168            }
1169        }
1170        Value::Symbol(id) => format!("\"{}\"", json_escape(&symbol::resolve(*id))),
1171        Value::String(s) => format!("\"{}\"", json_escape(s)),
1172    }
1173}
1174
1175fn json_optional_string(value: Option<&str>) -> String {
1176    match value {
1177        Some(value) => format!("\"{}\"", json_escape(value)),
1178        None => "null".to_string(),
1179    }
1180}
1181
1182fn json_escape(value: &str) -> String {
1183    let serialized = serde_json::to_string(value).expect("serializing a string cannot fail");
1184    serialized[1..serialized.len() - 1].to_string()
1185}
1186
1187fn dot_escape(value: &str) -> String {
1188    value.replace('\\', "\\\\").replace('"', "\\\"")
1189}
1190
1191fn memory_budget_bytes(memory_mb: u64) -> Result<u64> {
1192    memory_mb.checked_mul(1024 * 1024).ok_or_else(|| {
1193        XlogError::Execution(format!("memory budget {memory_mb} MiB overflows bytes"))
1194    })
1195}
1196
1197fn make_provider(device: usize, memory_mb: u64) -> Result<Arc<CudaKernelProvider>> {
1198    let memory_bytes = memory_budget_bytes(memory_mb)?;
1199    Ok(Arc::new(
1200        CudaProviderBuilder::new(device, MemoryBudget::with_limit(memory_bytes)).build()?,
1201    ))
1202}
1203
1204fn parse_inputs(inputs: &[String]) -> Result<HashMap<String, PathBuf>> {
1205    let mut out = HashMap::new();
1206    for entry in inputs {
1207        let (name, path) = entry.split_once('=').ok_or_else(|| {
1208            XlogError::Execution(format!("Invalid --input '{}', expected rel=path", entry))
1209        })?;
1210        out.insert(name.to_string(), PathBuf::from(path));
1211    }
1212    Ok(out)
1213}
1214
1215fn run_deterministic(args: RunArgs) -> Result<()> {
1216    if !args.materialize_relation.is_empty() && !matches!(args.output, OutputFormat::Json) {
1217        return Err(XlogError::Execution(
1218            "--materialize-relation requires --output json".to_string(),
1219        ));
1220    }
1221    if matches!(args.output, OutputFormat::Json) && args.output_dir.is_some() {
1222        return Err(XlogError::Execution(
1223            "--output-dir is not supported with --output json".to_string(),
1224        ));
1225    }
1226    if args.wcoj {
1227        // Force the WCOJ dispatch gates (default RuntimeConfig consults these
1228        // env vars; see xlog_core::RuntimeConfig::wcoj_triangle_dispatch).
1229        std::env::set_var("XLOG_USE_WCOJ_TRIANGLE_U32", "1");
1230        std::env::set_var("XLOG_USE_WCOJ_4CYCLE", "1");
1231        // This only sets the gate. Whether a WCOJ kernel actually dispatched
1232        // (vs. silently falling back to binary joins) is reported post-run
1233        // via --stats `wcoj.triangle_dispatch` / `wcoj.four_cycle_dispatch`.
1234        eprintln!("WCOJ dispatch gates set (triangle + 4-cycle); run with --stats to confirm a kernel fired");
1235    }
1236    let provider = make_provider(args.device, args.memory_mb)?;
1237    let source = std::fs::read_to_string(&args.source).map_err(|e| {
1238        XlogError::Execution(format!("Failed to read {}: {}", args.source.display(), e))
1239    })?;
1240
1241    let parsed = xlog_logic::parse_program(&source)?;
1242    let resolved = resolve_program_imports(
1243        parsed,
1244        &args.source,
1245        args.module_path.clone(),
1246        ModuleMergeErrorKind::Compilation,
1247    )?;
1248    let program = LogicProgram::compile_program(resolved)?;
1249    let mut inputs = HashMap::new();
1250    for (name, path) in parse_inputs(&args.input)? {
1251        let buf = provider.read_arrow_ipc_stream_file(&path)?;
1252        inputs.insert(name, buf);
1253    }
1254
1255    let (result, materialized_store) = if matches!(args.output, OutputFormat::Json) {
1256        let mut relation_store = program.create_relation_store(provider.clone())?;
1257        for (name, buffer) in inputs {
1258            relation_store.put(&name, buffer);
1259        }
1260        let (result, materialized_store) = program.evaluate_with_relation_store_and_cache(
1261            provider.clone(),
1262            &relation_store,
1263            args.stats,
1264        )?;
1265        (result, Some(materialized_store))
1266    } else {
1267        (
1268            program.evaluate_with_options(provider.clone(), inputs, args.stats)?,
1269            None,
1270        )
1271    };
1272
1273    // Dump the compiled epistemic execution plan after a successful GPU run, so
1274    // the JSON corresponds to a real accepted hot-path execution.
1275    if let Some(plan_path) = &args.epistemic_plan_json {
1276        match program.epistemic_plan_json() {
1277            Some(json) => {
1278                std::fs::write(plan_path, json).map_err(|e| {
1279                    XlogError::Execution(format!(
1280                        "Failed to write epistemic plan JSON {}: {}",
1281                        plan_path.display(),
1282                        e
1283                    ))
1284                })?;
1285                eprintln!("epistemic plan dumped to {}", plan_path.display());
1286            }
1287            None => {
1288                eprintln!(
1289                    "note: --epistemic-plan-json given but program has no epistemic literals; no plan dumped"
1290                );
1291            }
1292        }
1293    }
1294
1295    // Emit query results and, for the structured route, requested completed
1296    // runtime relations from the same evaluated store.
1297    if let Some(materialized_store) = materialized_store.as_ref() {
1298        emit_deterministic_json(
1299            provider.as_ref(),
1300            &result.queries,
1301            materialized_store.as_relation_store(),
1302            &args.materialize_relation,
1303        )?;
1304    } else {
1305        emit_logic_results(
1306            provider.as_ref(),
1307            &result.queries,
1308            args.output,
1309            args.output_dir.as_deref(),
1310        )?;
1311    }
1312
1313    // Emit stats if requested
1314    if args.stats {
1315        if let Some(stats) = result.stats {
1316            if args.wcoj {
1317                // Honest confirmation: did a WCOJ kernel actually fire, or did
1318                // the run silently fall back to binary joins? Count every WCOJ
1319                // dispatch kind — fused triangle COUNTING, for instance, routes
1320                // through the group-by-fusion kernel, not the triangle hook.
1321                let fired = stats.wcoj_triangle_dispatch_count
1322                    + stats.wcoj_4cycle_dispatch_count
1323                    + stats.wcoj_groupby_fusion_dispatch_count
1324                    + stats.free_join_dispatch_count
1325                    + stats.factorized_delta_dispatch_count;
1326                if fired > 0 {
1327                    eprintln!(
1328                        "WCOJ kernels dispatched: triangle {}, 4-cycle {}, groupby-fusion {}, free-join {}, factorized-delta {} (fallbacks {}, pipeline errors {})",
1329                        stats.wcoj_triangle_dispatch_count,
1330                        stats.wcoj_4cycle_dispatch_count,
1331                        stats.wcoj_groupby_fusion_dispatch_count,
1332                        stats.free_join_dispatch_count,
1333                        stats.factorized_delta_dispatch_count,
1334                        stats.wcoj_fallback.total(),
1335                        stats.wcoj_error_decline_count,
1336                    );
1337                } else {
1338                    eprintln!(
1339                        "WARNING: --wcoj set but no WCOJ kernel dispatched (fallbacks {}, pipeline errors {}); the run fell back to binary joins",
1340                        stats.wcoj_fallback.total(),
1341                        stats.wcoj_error_decline_count,
1342                    );
1343                }
1344            }
1345            let stats_output = match args.stats_format {
1346                StatsFormat::Human => stats.format_human(),
1347                StatsFormat::Json => stats.format_json(),
1348            };
1349            eprintln!("{}", stats_output);
1350        }
1351        // Symbol table statistics
1352        eprintln!(
1353            "Symbols: {} interned ({} bytes)",
1354            symbol::count(),
1355            symbol::memory_usage()
1356        );
1357    }
1358
1359    Ok(())
1360}
1361
1362fn run_probabilistic(args: ProbArgs) -> Result<()> {
1363    #[cfg(not(feature = "host-io"))]
1364    {
1365        let _ = args;
1366        Err(XlogError::Execution(
1367            "Host output is disabled (feature \"host-io\" is OFF). Use device-resident APIs (DLPack) or rebuild with --features host-io.".to_string(),
1368        ))
1369    }
1370
1371    #[cfg(feature = "host-io")]
1372    {
1373        let source = std::fs::read_to_string(&args.source).map_err(|e| {
1374            XlogError::Execution(format!("Failed to read {}: {}", args.source.display(), e))
1375        })?;
1376        let parsed_program = parse_program(&source)?;
1377        let program = resolve_program_imports(
1378            parsed_program,
1379            &args.source,
1380            args.module_path.clone(),
1381            ModuleMergeErrorKind::Execution,
1382        )?;
1383
1384        let mut config = GpuConfig::default();
1385        config.device_ordinal = args.device;
1386        config.memory_bytes = memory_budget_bytes(args.memory_mb)?;
1387
1388        match resolve_prob_engine(&args, &program) {
1389            ProbEngineCli::ExactDdnnf => {
1390                let prog = ExactDdnnfProgram::compile_from_program(&program, config)?;
1391                let result = prog.evaluate()?;
1392                emit_prob_exact(result, args.output, args.output_dir.as_deref())
1393            }
1394            ProbEngineCli::Mc => {
1395                let prog = McProgram::compile_from_program(&program, config)?;
1396                let mut cfg = McEvalConfig::from_directives(&program.directives)?;
1397                apply_mc_cli_overrides(&args, &mut cfg)?;
1398                // `evaluate` runs the GPU-native device hot loop and then
1399                // materializes the result on the host (downloads the final
1400                // query/evidence counts after the loop) so the CLI can print
1401                // probabilities and confidence intervals. The hot loop itself is
1402                // zero-host; this final download is host-result materialization,
1403                // not a hot-loop transfer. Device-resident consumers that want to
1404                // keep counts on the GPU use `McProgram::evaluate_gpu_device`.
1405                let result = prog.evaluate(cfg)?;
1406                emit_prob_mc(result, args.output, args.output_dir.as_deref())
1407            }
1408        }
1409    }
1410}
1411
1412#[cfg(feature = "host-io")]
1413fn resolve_prob_engine(args: &ProbArgs, program: &Program) -> ProbEngineCli {
1414    args.prob_engine
1415        .unwrap_or_else(|| match program.directives.prob_engine_or_default() {
1416            ProbEngine::ExactDdnnf => ProbEngineCli::ExactDdnnf,
1417            ProbEngine::Mc => ProbEngineCli::Mc,
1418        })
1419}
1420
1421#[cfg(feature = "host-io")]
1422fn apply_mc_cli_overrides(args: &ProbArgs, cfg: &mut McEvalConfig) -> Result<()> {
1423    cfg.apply_overrides(McEvalOverrides {
1424        samples: args.samples,
1425        seed: args.seed,
1426        confidence: args.confidence,
1427        max_nonmonotone_iterations: args.prob_max_nonmonotone_iterations,
1428        sampling_method: args.prob_method.map(|method| match method {
1429            ProbMethodCli::Rejection => McSamplingMethod::Rejection,
1430            ProbMethodCli::EvidenceClamping => McSamplingMethod::EvidenceClamping,
1431        }),
1432        allow_cpu_oracle_fallback: Some(args.allow_cpu_oracle),
1433    })
1434}
1435
1436fn emit_logic_results(
1437    provider: &CudaKernelProvider,
1438    queries: &[xlog_gpu::logic::LogicQueryResult],
1439    format: OutputFormat,
1440    output_dir: Option<&Path>,
1441) -> Result<()> {
1442    for (i, q) in queries.iter().enumerate() {
1443        if q.buffer.schema().arity() == 0 && matches!(format, OutputFormat::Pretty) {
1444            println!(
1445                "{}\nrows: {}",
1446                q.relation_name,
1447                provider.device_row_count(&q.buffer)?
1448            );
1449            continue;
1450        }
1451        let batch = provider.to_arrow_record_batch(&q.buffer)?;
1452        match format {
1453            OutputFormat::Pretty => {
1454                let formatted = pretty_format_batches(&[batch])
1455                    .map_err(|e| XlogError::Execution(format!("Pretty print failed: {}", e)))?;
1456                println!("{}\n{}", q.relation_name, formatted);
1457            }
1458            OutputFormat::Csv => {
1459                let mut out = Vec::new();
1460                {
1461                    let mut writer = WriterBuilder::new().build(&mut out);
1462                    writer
1463                        .write(&batch)
1464                        .map_err(|e| XlogError::Execution(format!("CSV write failed: {}", e)))?;
1465                }
1466                println!("{}\n{}", q.relation_name, String::from_utf8_lossy(&out));
1467            }
1468            OutputFormat::Arrow => {
1469                let dir = output_dir.unwrap_or_else(|| Path::new("."));
1470                let path = dir.join(format!("query_{}.arrow", i));
1471                provider.write_arrow_ipc_stream_file(&q.buffer, &path)?;
1472                println!("wrote {}", path.display());
1473            }
1474            OutputFormat::Json => {
1475                unreachable!("JSON output is emitted from the materialized store")
1476            }
1477        }
1478    }
1479    Ok(())
1480}
1481
1482fn emit_deterministic_json(
1483    provider: &CudaKernelProvider,
1484    queries: &[xlog_gpu::logic::LogicQueryResult],
1485    materialized_store: &xlog_runtime::RelationStore,
1486    requested_relations: &[String],
1487) -> Result<()> {
1488    let queries = queries
1489        .iter()
1490        .map(|query| {
1491            Ok(serde_json::json!({
1492                "relation_name": query.relation_name,
1493                "columns": query.columns,
1494                "scalar_types": scalar_type_labels(&query.buffer)?,
1495                "rows": logic_rows_json(provider, &query.buffer)?,
1496            }))
1497        })
1498        .collect::<Result<Vec<_>>>()?;
1499
1500    let relation_names = requested_relations.iter().cloned().collect::<BTreeSet<_>>();
1501    let materialized_relations = relation_names
1502        .into_iter()
1503        .map(|relation_name| {
1504            let buffer = materialized_store.get(&relation_name).ok_or_else(|| {
1505                XlogError::Execution(format!(
1506                    "requested materialized relation '{relation_name}' is not present in the completed runtime store"
1507                ))
1508            })?;
1509            Ok(serde_json::json!({
1510                "relation_name": relation_name,
1511                "scalar_types": scalar_type_labels(buffer)?,
1512                "rows": logic_rows_json(provider, buffer)?,
1513            }))
1514        })
1515        .collect::<Result<Vec<_>>>()?;
1516
1517    let payload = serde_json::json!({
1518        "schema_version": "xlog.deterministic-run.v1",
1519        "execution_engine": "xlog-gpu",
1520        "fixpoint_state": "complete",
1521        "materialization_transfer": "post_fixpoint_d2h",
1522        "queries": queries,
1523        "materialized_relations": materialized_relations,
1524    });
1525    let output = serde_json::to_string(&payload).map_err(|error| {
1526        XlogError::Execution(format!(
1527            "failed to serialize deterministic run JSON: {error}"
1528        ))
1529    })?;
1530    println!("{output}");
1531    Ok(())
1532}
1533
1534fn scalar_type_labels(buffer: &xlog_cuda::CudaBuffer) -> Result<Vec<&'static str>> {
1535    (0..buffer.schema().arity())
1536        .map(|column_index| {
1537            buffer
1538                .schema()
1539                .column_type(column_index)
1540                .map(scalar_type_label)
1541                .ok_or_else(|| {
1542                    XlogError::Execution(format!(
1543                        "relation schema is missing scalar type for column {column_index}"
1544                    ))
1545                })
1546        })
1547        .collect()
1548}
1549
1550fn scalar_type_label(scalar_type: ScalarType) -> &'static str {
1551    match scalar_type {
1552        ScalarType::U32 => "u32",
1553        ScalarType::U64 => "u64",
1554        ScalarType::I32 => "i32",
1555        ScalarType::I64 => "i64",
1556        ScalarType::F32 => "f32",
1557        ScalarType::F64 => "f64",
1558        ScalarType::Bool => "bool",
1559        ScalarType::Symbol => "symbol",
1560    }
1561}
1562
1563fn logic_rows_json(
1564    provider: &CudaKernelProvider,
1565    buffer: &xlog_cuda::CudaBuffer,
1566) -> Result<Vec<Vec<serde_json::Value>>> {
1567    download_logic_rows(provider, buffer)?
1568        .into_iter()
1569        .map(|row| row.into_iter().map(logic_scalar_json).collect())
1570        .collect()
1571}
1572
1573fn logic_scalar_json(value: LogicScalarValue) -> Result<serde_json::Value> {
1574    match value {
1575        LogicScalarValue::U32(value) => Ok(serde_json::json!(value)),
1576        LogicScalarValue::U64(value) => Ok(serde_json::json!(value)),
1577        LogicScalarValue::I32(value) => Ok(serde_json::json!(value)),
1578        LogicScalarValue::I64(value) => Ok(serde_json::json!(value)),
1579        LogicScalarValue::F32(value) => finite_json_number(f64::from(value), "f32"),
1580        LogicScalarValue::F64(value) => finite_json_number(value, "f64"),
1581        LogicScalarValue::Bool(value) => Ok(serde_json::json!(value)),
1582        LogicScalarValue::Symbol(value) => Ok(serde_json::json!(value)),
1583    }
1584}
1585
1586fn finite_json_number(value: f64, scalar_type: &str) -> Result<serde_json::Value> {
1587    serde_json::Number::from_f64(value)
1588        .map(serde_json::Value::Number)
1589        .ok_or_else(|| {
1590            XlogError::Execution(format!(
1591                "cannot serialize non-finite {scalar_type} relation value {value}"
1592            ))
1593        })
1594}
1595
1596#[cfg(feature = "host-io")]
1597fn emit_prob_exact(
1598    result: xlog_prob::exact::ExactResult,
1599    format: ProbOutputFormat,
1600    output_dir: Option<&Path>,
1601) -> Result<()> {
1602    if matches!(format, ProbOutputFormat::Json) {
1603        print_prob_exact_json(result);
1604        return Ok(());
1605    }
1606
1607    let mut atoms = Vec::new();
1608    let mut probs = Vec::new();
1609    let mut log_probs = Vec::new();
1610    for q in result.query_probs {
1611        atoms.push(atom_to_string(&q.atom));
1612        probs.push(q.prob);
1613        log_probs.push(q.log_prob);
1614    }
1615
1616    let batch = arrow::record_batch::RecordBatch::try_from_iter(vec![
1617        (
1618            "atom",
1619            Arc::new(arrow::array::StringArray::from(atoms)) as Arc<dyn arrow::array::Array>,
1620        ),
1621        (
1622            "prob",
1623            Arc::new(arrow::array::Float64Array::from(probs)) as Arc<dyn arrow::array::Array>,
1624        ),
1625        (
1626            "log_prob",
1627            Arc::new(arrow::array::Float64Array::from(log_probs)) as Arc<dyn arrow::array::Array>,
1628        ),
1629    ])
1630    .map_err(|e| XlogError::Execution(format!("Failed to build prob batch: {}", e)))?;
1631
1632    emit_batch(
1633        "prob",
1634        &batch,
1635        prob_output_as_batch_format(format),
1636        output_dir,
1637    )
1638}
1639
1640#[cfg(feature = "host-io")]
1641fn emit_prob_mc(
1642    result: xlog_prob::mc::McResult,
1643    format: ProbOutputFormat,
1644    output_dir: Option<&Path>,
1645) -> Result<()> {
1646    if matches!(format, ProbOutputFormat::Json) {
1647        print_prob_mc_json(result);
1648        return Ok(());
1649    }
1650
1651    let total_samples = result.total_samples as u64;
1652    let evidence_samples = result.evidence_samples as u64;
1653    let seed = result.seed;
1654    let confidence = result.confidence;
1655    let sampling_method = result.sampling_method.as_str().to_string();
1656    let mc_engine = result.engine.as_str().to_string();
1657
1658    let mut atoms = Vec::new();
1659    let mut probs = Vec::new();
1660    let mut log_probs = Vec::new();
1661    let mut stderr = Vec::new();
1662    let mut ci_low = Vec::new();
1663    let mut ci_high = Vec::new();
1664    let mut total_samples_col = Vec::new();
1665    let mut evidence_samples_col = Vec::new();
1666    let mut seed_col = Vec::new();
1667    let mut confidence_col = Vec::new();
1668    let mut sampling_method_col = Vec::new();
1669    let mut mc_engine_col = Vec::new();
1670    for q in result.query_estimates {
1671        atoms.push(atom_to_string(&q.atom));
1672        probs.push(q.prob);
1673        log_probs.push(q.log_prob);
1674        stderr.push(q.stderr);
1675        ci_low.push(q.ci_low);
1676        ci_high.push(q.ci_high);
1677        total_samples_col.push(total_samples);
1678        evidence_samples_col.push(evidence_samples);
1679        seed_col.push(seed);
1680        confidence_col.push(confidence);
1681        sampling_method_col.push(sampling_method.clone());
1682        mc_engine_col.push(mc_engine.clone());
1683    }
1684
1685    let batch = arrow::record_batch::RecordBatch::try_from_iter(vec![
1686        (
1687            "atom",
1688            Arc::new(arrow::array::StringArray::from(atoms)) as Arc<dyn arrow::array::Array>,
1689        ),
1690        (
1691            "prob",
1692            Arc::new(arrow::array::Float64Array::from(probs)) as Arc<dyn arrow::array::Array>,
1693        ),
1694        (
1695            "log_prob",
1696            Arc::new(arrow::array::Float64Array::from(log_probs)) as Arc<dyn arrow::array::Array>,
1697        ),
1698        (
1699            "stderr",
1700            Arc::new(arrow::array::Float64Array::from(stderr)) as Arc<dyn arrow::array::Array>,
1701        ),
1702        (
1703            "ci_low",
1704            Arc::new(arrow::array::Float64Array::from(ci_low)) as Arc<dyn arrow::array::Array>,
1705        ),
1706        (
1707            "ci_high",
1708            Arc::new(arrow::array::Float64Array::from(ci_high)) as Arc<dyn arrow::array::Array>,
1709        ),
1710        (
1711            "total_samples",
1712            Arc::new(arrow::array::UInt64Array::from(total_samples_col))
1713                as Arc<dyn arrow::array::Array>,
1714        ),
1715        (
1716            "evidence_samples",
1717            Arc::new(arrow::array::UInt64Array::from(evidence_samples_col))
1718                as Arc<dyn arrow::array::Array>,
1719        ),
1720        (
1721            "seed",
1722            Arc::new(arrow::array::UInt64Array::from(seed_col)) as Arc<dyn arrow::array::Array>,
1723        ),
1724        (
1725            "confidence",
1726            Arc::new(arrow::array::Float64Array::from(confidence_col))
1727                as Arc<dyn arrow::array::Array>,
1728        ),
1729        (
1730            "sampling_method",
1731            Arc::new(arrow::array::StringArray::from(sampling_method_col))
1732                as Arc<dyn arrow::array::Array>,
1733        ),
1734        (
1735            "mc_engine",
1736            Arc::new(arrow::array::StringArray::from(mc_engine_col))
1737                as Arc<dyn arrow::array::Array>,
1738        ),
1739    ])
1740    .map_err(|e| XlogError::Execution(format!("Failed to build mc batch: {}", e)))?;
1741
1742    emit_batch(
1743        "prob",
1744        &batch,
1745        prob_output_as_batch_format(format),
1746        output_dir,
1747    )
1748}
1749
1750#[cfg(feature = "host-io")]
1751fn prob_output_as_batch_format(format: ProbOutputFormat) -> OutputFormat {
1752    match format {
1753        ProbOutputFormat::Pretty => OutputFormat::Pretty,
1754        ProbOutputFormat::Csv => OutputFormat::Csv,
1755        ProbOutputFormat::Arrow => OutputFormat::Arrow,
1756        ProbOutputFormat::Json => unreachable!("json output is handled before batch emission"),
1757    }
1758}
1759
1760#[cfg(feature = "host-io")]
1761fn print_prob_exact_json(result: xlog_prob::exact::ExactResult) {
1762    println!("{{");
1763    println!("  \"engine\": \"exact_ddnnf\",");
1764    println!("  \"queries\": [");
1765    let len = result.query_probs.len();
1766    for (idx, q) in result.query_probs.into_iter().enumerate() {
1767        let suffix = if idx + 1 == len { "" } else { "," };
1768        println!("    {{");
1769        println!(
1770            "      \"atom\": \"{}\",",
1771            json_escape(&atom_to_string(&q.atom))
1772        );
1773        println!("      \"prob\": {},", q.prob);
1774        println!("      \"log_prob\": {}", q.log_prob);
1775        println!("    }}{}", suffix);
1776    }
1777    println!("  ]");
1778    println!("}}");
1779}
1780
1781#[cfg(feature = "host-io")]
1782fn print_prob_mc_json(result: xlog_prob::mc::McResult) {
1783    let total_samples = result.total_samples;
1784    let evidence_samples = result.evidence_samples;
1785    let seed = result.seed;
1786    let confidence = result.confidence;
1787    let sampling_method = result.sampling_method.as_str();
1788    let mc_engine = result.engine.as_str();
1789    println!("{{");
1790    println!("  \"engine\": \"mc\",");
1791    println!("  \"mc_engine\": \"{}\",", mc_engine);
1792    println!("  \"total_samples\": {},", total_samples);
1793    println!("  \"evidence_samples\": {},", evidence_samples);
1794    println!("  \"seed\": {},", seed);
1795    println!("  \"confidence\": {},", confidence);
1796    println!("  \"sampling_method\": \"{}\",", sampling_method);
1797    println!("  \"queries\": [");
1798    let len = result.query_estimates.len();
1799    for (idx, q) in result.query_estimates.into_iter().enumerate() {
1800        let suffix = if idx + 1 == len { "" } else { "," };
1801        println!("    {{");
1802        println!(
1803            "      \"atom\": \"{}\",",
1804            json_escape(&atom_to_string(&q.atom))
1805        );
1806        println!("      \"prob\": {},", q.prob);
1807        println!("      \"log_prob\": {},", q.log_prob);
1808        println!("      \"stderr\": {},", q.stderr);
1809        println!("      \"ci_low\": {},", q.ci_low);
1810        println!("      \"ci_high\": {},", q.ci_high);
1811        println!("      \"total_samples\": {},", total_samples);
1812        println!("      \"evidence_samples\": {}", evidence_samples);
1813        println!("    }}{}", suffix);
1814    }
1815    println!("  ]");
1816    println!("}}");
1817}
1818
1819#[cfg(feature = "host-io")]
1820fn emit_batch(
1821    name: &str,
1822    batch: &arrow::record_batch::RecordBatch,
1823    format: OutputFormat,
1824    output_dir: Option<&Path>,
1825) -> Result<()> {
1826    match format {
1827        OutputFormat::Pretty => {
1828            let formatted = pretty_format_batches(std::slice::from_ref(batch))
1829                .map_err(|e| XlogError::Execution(format!("Pretty print failed: {}", e)))?;
1830            println!("{}\n{}", name, formatted);
1831        }
1832        OutputFormat::Csv => {
1833            let mut out = Vec::new();
1834            {
1835                let mut writer = WriterBuilder::new().build(&mut out);
1836                writer
1837                    .write(batch)
1838                    .map_err(|e| XlogError::Execution(format!("CSV write failed: {}", e)))?;
1839            }
1840            println!("{}\n{}", name, String::from_utf8_lossy(&out));
1841        }
1842        OutputFormat::Arrow => {
1843            let dir = output_dir.unwrap_or_else(|| Path::new("."));
1844            let path = dir.join(format!("{}_prob.arrow", name));
1845            let mut out = Vec::new();
1846            let mut writer =
1847                arrow::ipc::writer::StreamWriter::try_new(&mut out, &batch.schema())
1848                    .map_err(|e| XlogError::Execution(format!("Arrow writer failed: {}", e)))?;
1849            writer
1850                .write(batch)
1851                .map_err(|e| XlogError::Execution(format!("Arrow write failed: {}", e)))?;
1852            writer
1853                .finish()
1854                .map_err(|e| XlogError::Execution(format!("Arrow finish failed: {}", e)))?;
1855            std::fs::write(&path, out)
1856                .map_err(|e| XlogError::Execution(format!("Arrow write file failed: {}", e)))?;
1857            println!("wrote {}", path.display());
1858        }
1859        OutputFormat::Json => unreachable!("probabilistic JSON is emitted before batch output"),
1860    }
1861    Ok(())
1862}
1863
1864#[cfg(feature = "host-io")]
1865fn atom_to_string(atom: &xlog_prob::provenance::GroundAtom) -> String {
1866    use xlog_prob::provenance::Value;
1867
1868    if atom.args.is_empty() {
1869        return format!("{}()", atom.predicate);
1870    }
1871
1872    let mut out = String::new();
1873    out.push_str(&atom.predicate);
1874    out.push('(');
1875    for (i, arg) in atom.args.iter().enumerate() {
1876        if i != 0 {
1877            out.push_str(", ");
1878        }
1879        match arg {
1880            Value::I64(v) => out.push_str(&v.to_string()),
1881            Value::F64(bits) => out.push_str(&f64::from_bits(*bits).to_string()),
1882            Value::Symbol(sym) => out.push_str(&symbol::resolve(*sym)),
1883            Value::String(v) => out.push_str(v),
1884        }
1885    }
1886    out.push(')');
1887    out
1888}