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