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 #[arg(long)]
68 stats: bool,
69 #[arg(long, value_enum, default_value = "human")]
71 stats_format: StatsFormat,
72 #[arg(long, value_delimiter = ':')]
74 module_path: Vec<PathBuf>,
75 #[arg(long)]
81 epistemic_plan_json: Option<PathBuf>,
82 #[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 #[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 #[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 #[arg(long, value_delimiter = ':')]
137 module_path: Vec<PathBuf>,
138}
139
140#[derive(Parser)]
141struct ReplArgs {
142 #[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 #[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
268fn 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 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 std::env::set_var("XLOG_USE_WCOJ_TRIANGLE_U32", "1");
1204 std::env::set_var("XLOG_USE_WCOJ_4CYCLE", "1");
1205 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 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_logic_results(
1255 provider.as_ref(),
1256 &result.queries,
1257 args.output,
1258 args.output_dir.as_deref(),
1259 )?;
1260
1261 if args.stats {
1263 if let Some(stats) = result.stats {
1264 if args.wcoj {
1265 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 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 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}