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