Skip to main content

xlog/
main.rs

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