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