1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
4
5use xlog_core::{symbol, ScalarType};
6
7use crate::ast::{AggOp, ArithExpr, Atom, BodyLiteral, CompOp, Program, Rule, Term};
8use crate::expand::generated_function_variable_source;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum RuleSourceKind {
13 Source,
15 Generated,
17 Mined,
19 Imported,
21 RuntimeInjected,
23}
24
25impl RuleSourceKind {
26 pub fn as_str(self) -> &'static str {
28 match self {
29 RuleSourceKind::Source => "source",
30 RuleSourceKind::Generated => "generated",
31 RuleSourceKind::Mined => "mined",
32 RuleSourceKind::Imported => "imported",
33 RuleSourceKind::RuntimeInjected => "runtime_injected",
34 }
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct RuleProvenance {
41 pub rule_id: String,
43 pub head: String,
45 pub source_kind: RuleSourceKind,
47 pub source_span: Option<String>,
49 pub generation_trace_hash: Option<String>,
51 pub support_relation_ids: Vec<String>,
53 pub counterexample_relation_ids: Vec<String>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct QueryProofTrace {
60 pub query_id: String,
62 pub query: String,
64 pub answer_relation: String,
66 pub rule_ids: Vec<String>,
68 pub source_facts: Vec<String>,
70 pub rejected_alternatives: Vec<String>,
72}
73
74pub fn rule_provenance(
76 program: &Program,
77 generated_program: Option<&Program>,
78) -> Vec<RuleProvenance> {
79 let mut out = Vec::new();
80 let mut source_keys = BTreeSet::new();
81
82 for (idx, rule) in program.rules.iter().enumerate() {
83 source_keys.insert(rule_key(rule));
84 out.push(rule_record(idx, rule, RuleSourceKind::Source));
85 }
86
87 if let Some(generated) = generated_program {
88 let mut generated_idx = 0usize;
89 for rule in &generated.rules {
90 if source_keys.contains(&rule_key(rule)) {
91 continue;
92 }
93 out.push(rule_record(generated_idx, rule, RuleSourceKind::Generated));
94 generated_idx += 1;
95 }
96 }
97
98 out
99}
100
101pub fn build_rule_provenance(
103 program: &Program,
104 generated_predicates: &[String],
105) -> Vec<RuleProvenance> {
106 let mut out = rule_provenance(program, None);
107 for (idx, predicate) in generated_predicates.iter().enumerate() {
108 out.push(RuleProvenance {
109 rule_id: format!("rule:generated:{}:{}", idx, predicate),
110 head: predicate.clone(),
111 source_kind: RuleSourceKind::Generated,
112 source_span: None,
113 generation_trace_hash: Some(stable_hash(&format!("generated:{}", predicate))),
114 support_relation_ids: vec![predicate.clone()],
115 counterexample_relation_ids: Vec::new(),
116 });
117 }
118 out
119}
120
121pub fn query_proof_traces(
123 program: &Program,
124 provenance: &[RuleProvenance],
125) -> Vec<QueryProofTrace> {
126 let mut rule_ids_by_head: BTreeMap<String, Vec<String>> = BTreeMap::new();
127 for entry in provenance {
128 rule_ids_by_head
129 .entry(head_predicate(&entry.head).to_string())
130 .or_default()
131 .push(entry.rule_id.clone());
132 }
133
134 program
135 .queries
136 .iter()
137 .enumerate()
138 .map(|(idx, query)| {
139 let query_pred = query.atom.predicate.clone();
140 let deriving_rules: Vec<&Rule> = program
141 .rules
142 .iter()
143 .filter(|rule| !rule.is_fact() && rule.head.predicate == query_pred)
144 .collect();
145 let rule_ids = rule_ids_by_head
146 .get(&query_pred)
147 .cloned()
148 .unwrap_or_default();
149 let source_facts = source_facts_for_rules(program, &deriving_rules);
150 let rejected_alternatives = deriving_rules
151 .iter()
152 .flat_map(|rule| {
153 rule.body.iter().filter_map(|lit| match lit {
154 BodyLiteral::Negated(atom) => Some(format!("not {}", format_atom(atom))),
155 _ => None,
156 })
157 })
158 .collect::<Vec<_>>();
159
160 QueryProofTrace {
161 query_id: format!("query:source:{}:{}", idx, format_atom(&query.atom)),
162 query: format_atom(&query.atom),
163 answer_relation: format!("__xlog_query_{}", idx),
164 rule_ids,
165 source_facts,
166 rejected_alternatives,
167 }
168 })
169 .collect()
170}
171
172pub fn build_query_proof_traces(program: &Program) -> Vec<QueryProofTrace> {
174 let provenance = rule_provenance(program, None);
175 query_proof_traces(program, &provenance)
176}
177
178pub fn source_diagnostics(
184 source_program: &Program,
185 analysis_program: &Program,
186 rewritten_program: Option<&Program>,
187) -> (Vec<RuleProvenance>, Vec<QueryProofTrace>) {
188 let mut provenance = rule_provenance(source_program, None);
189 let rules_align = analysis_program.rules.len() >= source_program.rules.len()
190 && source_program
191 .rules
192 .iter()
193 .zip(&analysis_program.rules)
194 .all(|(source, normalized)| {
195 source.head.predicate == normalized.head.predicate
196 && source.head.arity() == normalized.head.arity()
197 });
198 if rules_align {
199 let normalized = rule_provenance(analysis_program, None);
200 for (source, normalized) in provenance.iter_mut().zip(normalized) {
201 source.support_relation_ids = source
202 .support_relation_ids
203 .iter()
204 .chain(&normalized.support_relation_ids)
205 .filter(|name| !name.starts_with("__"))
206 .cloned()
207 .collect::<BTreeSet<_>>()
208 .into_iter()
209 .collect();
210 }
211 }
212
213 if let Some(rewritten_program) = rewritten_program {
214 provenance.extend(
215 rule_provenance(analysis_program, Some(rewritten_program))
216 .into_iter()
217 .filter(|entry| entry.source_kind == RuleSourceKind::Generated),
218 );
219 }
220
221 let mut proof_traces = query_proof_traces(source_program, &provenance);
222 let queries_align = source_program.queries.len() == analysis_program.queries.len()
223 && source_program
224 .queries
225 .iter()
226 .zip(&analysis_program.queries)
227 .all(|(source, normalized)| {
228 source.atom.predicate == normalized.atom.predicate
229 && source.atom.arity() == normalized.atom.arity()
230 });
231 if rules_align && queries_align {
232 let normalized_provenance = rule_provenance(analysis_program, None);
233 let normalized_traces = query_proof_traces(analysis_program, &normalized_provenance);
234 let function_variable_sources =
235 generated_function_variable_sources(source_program, analysis_program);
236 let authored_facts = source_program
237 .facts()
238 .map(|rule| format!("{}.", format_atom(&rule.head)))
239 .collect::<BTreeSet<_>>();
240 for (source, normalized) in proof_traces.iter_mut().zip(normalized_traces) {
241 source.source_facts = source
242 .source_facts
243 .iter()
244 .chain(&normalized.source_facts)
245 .filter(|fact| authored_facts.contains(*fact) && !fact.starts_with("__"))
246 .cloned()
247 .collect::<BTreeSet<_>>()
248 .into_iter()
249 .collect();
250 source.rejected_alternatives = source
251 .rejected_alternatives
252 .iter()
253 .cloned()
254 .chain(normalized.rejected_alternatives.iter().map(|alternative| {
255 source_format_normalized_alternative(alternative, &function_variable_sources)
256 }))
257 .filter(|alternative| {
258 !alternative
259 .strip_prefix("not ")
260 .unwrap_or(alternative)
261 .starts_with("__")
262 })
263 .collect::<BTreeSet<_>>()
264 .into_iter()
265 .collect();
266 }
267 }
268 (provenance, proof_traces)
269}
270
271pub fn generated_function_variable_sources(
273 source_program: &Program,
274 analysis_program: &Program,
275) -> HashMap<String, String> {
276 let authored_variables = program_variable_names(source_program);
277 let mut function_locals = source_program
278 .functions
279 .iter()
280 .filter_map(|function| {
281 let crate::ast::FuncBody::Predicate { result, body } = &function.body else {
282 return None;
283 };
284 let parameters = function
285 .params
286 .iter()
287 .map(|parameter| parameter.name.as_str())
288 .collect::<HashSet<_>>();
289 let mut locals = body
290 .iter()
291 .flat_map(BodyLiteral::variables)
292 .filter(|name| !parameters.contains(name))
293 .map(ToOwned::to_owned)
294 .collect::<HashSet<_>>();
295 if !parameters.contains(result.as_str()) {
296 locals.insert(result.clone());
297 }
298 Some((function.name.clone(), locals))
299 })
300 .collect::<Vec<_>>();
301 function_locals.sort_by_key(|(function, _)| std::cmp::Reverse(function.len()));
302
303 program_variable_names(analysis_program)
304 .into_iter()
305 .filter(|name| !authored_variables.contains(name))
306 .filter_map(|name| {
307 let source_name = function_locals.iter().find_map(|(function, locals)| {
308 let source_name = generated_function_variable_source(&name, function)?;
309 locals
310 .contains(source_name)
311 .then(|| source_name.to_string())
312 })?;
313 Some((name, source_name))
314 })
315 .collect()
316}
317
318fn program_variable_names(program: &Program) -> HashSet<String> {
319 let mut variables = HashSet::new();
320 for rule in &program.rules {
321 variables.extend(rule.head.variables().into_iter().map(ToOwned::to_owned));
322 variables.extend(
323 rule.body
324 .iter()
325 .flat_map(BodyLiteral::variables)
326 .map(ToOwned::to_owned),
327 );
328 }
329 for constraint in &program.constraints {
330 variables.extend(
331 constraint
332 .body
333 .iter()
334 .flat_map(BodyLiteral::variables)
335 .map(ToOwned::to_owned),
336 );
337 }
338 for query in &program.queries {
339 variables.extend(query.atom.variables().into_iter().map(ToOwned::to_owned));
340 }
341 variables
342}
343
344pub fn source_format_normalized_alternative(
346 alternative: &str,
347 function_variable_sources: &HashMap<String, String>,
348) -> String {
349 let characters = alternative.chars().collect::<Vec<_>>();
350 let mut output = String::with_capacity(alternative.len());
351 let mut index = 0;
352 let mut quoted = false;
353 let mut escaped = false;
354 while index < characters.len() {
355 let character = characters[index];
356 if quoted {
357 output.push(character);
358 if escaped {
359 escaped = false;
360 } else if character == '\\' {
361 escaped = true;
362 } else if character == '"' {
363 quoted = false;
364 }
365 index += 1;
366 continue;
367 }
368 if character == '"' {
369 quoted = true;
370 output.push(character);
371 index += 1;
372 continue;
373 }
374 if character.is_ascii_alphanumeric() || character == '_' {
375 let start = index;
376 while index < characters.len()
377 && (characters[index].is_ascii_alphanumeric() || characters[index] == '_')
378 {
379 index += 1;
380 }
381 let token = characters[start..index].iter().collect::<String>();
382 output.push_str(
383 function_variable_sources
384 .get(&token)
385 .map(String::as_str)
386 .unwrap_or(&token),
387 );
388 continue;
389 }
390 output.push(character);
391 index += 1;
392 }
393 output
394}
395
396fn rule_record(idx: usize, rule: &Rule, source_kind: RuleSourceKind) -> RuleProvenance {
397 let head = format_atom(&rule.head);
398 let prefix = source_kind.as_str();
399 RuleProvenance {
400 rule_id: format!("rule:{}:{}:{}", prefix, idx, stable_hash(&rule_key(rule))),
401 head,
402 source_kind,
403 source_span: Some(format!("rule_index:{}", idx)),
404 generation_trace_hash: Some(stable_hash(&rule_key(rule))),
405 support_relation_ids: support_relation_ids(rule),
406 counterexample_relation_ids: Vec::new(),
407 }
408}
409
410fn support_relation_ids(rule: &Rule) -> Vec<String> {
411 rule.body_predicates()
412 .into_iter()
413 .map(str::to_string)
414 .collect::<BTreeSet<_>>()
415 .into_iter()
416 .collect()
417}
418
419fn source_facts_for_rules(program: &Program, rules: &[&Rule]) -> Vec<String> {
420 let wanted: BTreeSet<String> = rules
421 .iter()
422 .flat_map(|rule| {
423 rule.body
424 .iter()
425 .filter_map(|lit| lit.atom().map(|atom| atom.predicate.clone()))
426 })
427 .collect();
428
429 let mut facts = BTreeSet::new();
430 for fact in program.facts() {
431 if wanted.contains(&fact.head.predicate) {
432 facts.insert(format!("{}.", format_atom(&fact.head)));
433 }
434 }
435 facts.into_iter().collect()
436}
437
438fn rule_key(rule: &Rule) -> String {
439 let mut out = format_atom(&rule.head);
440 if !rule.body.is_empty() {
441 let body = rule
442 .body
443 .iter()
444 .map(format_body_literal)
445 .collect::<Vec<_>>()
446 .join(", ");
447 out.push_str(" :- ");
448 out.push_str(&body);
449 }
450 out
451}
452
453fn head_predicate(head: &str) -> &str {
454 head.split_once('(').map(|(pred, _)| pred).unwrap_or(head)
455}
456
457pub fn format_atom(atom: &Atom) -> String {
459 let args = atom
460 .terms
461 .iter()
462 .map(format_term)
463 .collect::<Vec<_>>()
464 .join(", ");
465 format!("{}({})", atom.predicate, args)
466}
467
468pub fn format_constraint_body(body: &[BodyLiteral]) -> String {
470 let literals = body
471 .iter()
472 .map(format_body_literal)
473 .collect::<Vec<_>>()
474 .join(", ");
475 format!(":- {}.", literals)
476}
477
478fn format_body_literal(lit: &BodyLiteral) -> String {
479 match lit {
480 BodyLiteral::Positive(atom) => format_atom(atom),
481 BodyLiteral::Negated(atom) => format!("not {}", format_atom(atom)),
482 BodyLiteral::Epistemic(lit) => format_epistemic_literal(lit),
483 BodyLiteral::Comparison(comparison) => format!(
484 "{} {} {}",
485 format_term(&comparison.left),
486 format_comp_op(comparison.op),
487 format_term(&comparison.right)
488 ),
489 BodyLiteral::IsExpr(is_expr) => {
490 format!("{} is {}", is_expr.target, format_arith_expr(&is_expr.expr))
491 }
492 BodyLiteral::Univ(univ) => {
493 format!(
494 "{} =.. {}",
495 format_term(&univ.term),
496 format_term(&univ.parts)
497 )
498 }
499 }
500}
501
502fn format_epistemic_literal(lit: &crate::ast::EpistemicLiteral) -> String {
503 let op = match lit.op {
504 crate::ast::EpistemicOp::Know => "know",
505 crate::ast::EpistemicOp::Possible => "possible",
506 };
507 if lit.negated {
508 format!("not {} {}", op, format_atom(&lit.atom))
509 } else {
510 format!("{} {}", op, format_atom(&lit.atom))
511 }
512}
513
514pub fn format_term(term: &Term) -> String {
516 match term {
517 Term::Variable(name) => name.clone(),
518 Term::Anonymous => "_".to_string(),
519 Term::Integer(value) => value.to_string(),
520 Term::Float(value) => value.to_string(),
521 Term::String(value) => format!("\"{}\"", escape_string_contents(value)),
522 Term::Symbol(id) => symbol::resolve(*id),
523 Term::List(items) => {
524 let values = items.iter().map(format_term).collect::<Vec<_>>().join(", ");
525 format!("[{}]", values)
526 }
527 Term::Cons { head, tail } => {
528 format!("[{} | {}]", format_term(head), format_term(tail))
529 }
530 Term::Compound { functor, args } => {
531 let values = args.iter().map(format_term).collect::<Vec<_>>().join(", ");
532 format!("{}({})", functor, values)
533 }
534 Term::PredRef(name) => name.clone(),
535 Term::Aggregate(agg) => format!("{}({})", format_agg_op(agg.op), agg.variable),
536 }
537}
538
539fn escape_string_contents(value: &str) -> String {
540 let mut escaped = String::with_capacity(value.len());
541 for character in value.chars() {
542 match character {
543 '\\' => escaped.push_str("\\\\"),
544 '"' => escaped.push_str("\\\""),
545 '\n' => escaped.push_str("\\n"),
546 '\r' => escaped.push_str("\\r"),
547 '\t' => escaped.push_str("\\t"),
548 character if character.is_control() => escaped.extend(character.escape_default()),
549 character => escaped.push(character),
550 }
551 }
552 escaped
553}
554
555fn format_arith_expr(expr: &ArithExpr) -> String {
556 match expr {
557 ArithExpr::Variable(name) => name.clone(),
558 ArithExpr::Integer(value) => value.to_string(),
559 ArithExpr::Float(value) => value.to_string(),
560 ArithExpr::Add(left, right) => {
561 format!(
562 "({} + {})",
563 format_arith_expr(left),
564 format_arith_expr(right)
565 )
566 }
567 ArithExpr::Sub(left, right) => {
568 format!(
569 "({} - {})",
570 format_arith_expr(left),
571 format_arith_expr(right)
572 )
573 }
574 ArithExpr::Mul(left, right) => {
575 format!(
576 "({} * {})",
577 format_arith_expr(left),
578 format_arith_expr(right)
579 )
580 }
581 ArithExpr::Div(left, right) => {
582 format!(
583 "({} / {})",
584 format_arith_expr(left),
585 format_arith_expr(right)
586 )
587 }
588 ArithExpr::Mod(left, right) => {
589 format!(
590 "({} % {})",
591 format_arith_expr(left),
592 format_arith_expr(right)
593 )
594 }
595 ArithExpr::Abs(value) => format!("abs({})", format_arith_expr(value)),
596 ArithExpr::Min(left, right) => {
597 format!(
598 "min({}, {})",
599 format_arith_expr(left),
600 format_arith_expr(right)
601 )
602 }
603 ArithExpr::Max(left, right) => {
604 format!(
605 "max({}, {})",
606 format_arith_expr(left),
607 format_arith_expr(right)
608 )
609 }
610 ArithExpr::Pow(left, right) => {
611 format!(
612 "pow({}, {})",
613 format_arith_expr(left),
614 format_arith_expr(right)
615 )
616 }
617 ArithExpr::Cast(value, ty) => {
618 format!(
619 "cast({}, {})",
620 format_arith_expr(value),
621 format_scalar_type(*ty)
622 )
623 }
624 ArithExpr::FuncCall { name, args } => {
625 let values = args
626 .iter()
627 .map(format_arith_expr)
628 .collect::<Vec<_>>()
629 .join(", ");
630 format!("{}({})", name, values)
631 }
632 ArithExpr::Conditional {
633 cond_left,
634 cond_op,
635 cond_right,
636 then_expr,
637 else_expr,
638 } => format!(
639 "if {} {} {} then {} else {}",
640 format_arith_expr(cond_left),
641 format_comp_op(*cond_op),
642 format_arith_expr(cond_right),
643 format_arith_expr(then_expr),
644 format_arith_expr(else_expr)
645 ),
646 }
647}
648
649pub(crate) fn format_scalar_type(typ: ScalarType) -> &'static str {
650 match typ {
651 ScalarType::U32 => "u32",
652 ScalarType::U64 => "u64",
653 ScalarType::I32 => "i32",
654 ScalarType::I64 => "i64",
655 ScalarType::F32 => "f32",
656 ScalarType::F64 => "f64",
657 ScalarType::Bool => "bool",
658 ScalarType::Symbol => "symbol",
659 }
660}
661
662fn format_comp_op(op: CompOp) -> &'static str {
663 match op {
664 CompOp::Eq => "==",
665 CompOp::Ne => "!=",
666 CompOp::Lt => "<",
667 CompOp::Le => "<=",
668 CompOp::Gt => ">",
669 CompOp::Ge => ">=",
670 }
671}
672
673fn format_agg_op(op: AggOp) -> &'static str {
674 match op {
675 AggOp::Count => "count",
676 AggOp::Sum => "sum",
677 AggOp::Min => "min",
678 AggOp::Max => "max",
679 AggOp::LogSumExp => "logsumexp",
680 }
681}
682
683fn stable_hash(value: &str) -> String {
684 let mut hash = 0xcbf29ce484222325u64;
685 for byte in value.as_bytes() {
686 hash ^= u64::from(*byte);
687 hash = hash.wrapping_mul(0x100000001b3);
688 }
689 format!("{:016x}", hash)
690}
691
692#[cfg(test)]
693mod tests {
694 use std::collections::HashMap;
695
696 use super::{
697 format_constraint_body, generated_function_variable_sources,
698 source_format_normalized_alternative,
699 };
700
701 #[test]
702 fn constraint_formatter_uses_source_syntax_for_function_expressions() {
703 let program = crate::parse_program(
704 "func get_parent(Child) = Parent :- parent(Child, Parent).\n\
705 :- Parent is cast(get_parent(1), u64).",
706 )
707 .expect("parse constraint");
708
709 assert_eq!(
710 format_constraint_body(&program.constraints[0].body),
711 ":- Parent is cast(get_parent(1), u64)."
712 );
713 }
714
715 #[test]
716 fn generated_variable_mapping_handles_overlapping_function_names_and_underscores() {
717 let source = crate::parse_program(
718 "pred first(i32, i32).\n\
719 pred second(i32, i32).\n\
720 pred answer(i32, i32).\n\
721 first(1, 2).\n\
722 second(1, 3).\n\
723 func get(X) = Parent_Value :- first(X, Parent_Value).\n\
724 func get_parent(X) = Value_With_Underscore :- second(X, Value_With_Underscore).\n\
725 answer(A, B) :- A is get(1), B is get_parent(1).\n\
726 ?- answer(A, B).",
727 )
728 .expect("parse predicate functions");
729 let expanded =
730 crate::expand_program_functions(&source, 1_000).expect("expand predicate functions");
731 let mapping = generated_function_variable_sources(&source, &expanded);
732 assert!(mapping.values().any(|name| name == "Parent_Value"));
733 assert!(mapping.values().any(|name| name == "Value_With_Underscore"));
734 }
735
736 #[test]
737 fn normalized_alternative_mapping_does_not_rewrite_quoted_tokens() {
738 let generated = "__XLOG_FUNCTION_VISIBLE_Result_Value_7";
739 let mapping = HashMap::from([(generated.to_string(), "Result_Value".to_string())]);
740 assert_eq!(
741 source_format_normalized_alternative(
742 &format!("not blocked({generated}, \"{generated}\")"),
743 &mapping,
744 ),
745 format!("not blocked(Result_Value, \"{generated}\")")
746 );
747 }
748
749 #[test]
750 fn normalized_alternative_mapping_preserves_escaped_string_boundaries() {
751 let generated = "__XLOG_FUNCTION_VISIBLE_Result_Value_7";
752 let mapping = HashMap::from([(generated.to_string(), "Result_Value".to_string())]);
753 let mut literal = format!("quoted \" {generated} ");
754 literal.push('\\');
755 literal.push('\n');
756 literal.push('\r');
757 literal.push('\t');
758 literal.push('\u{0007}');
759 let atom = crate::ast::Atom {
760 predicate: "blocked".to_string(),
761 terms: vec![crate::ast::Term::String(literal)],
762 };
763 let formatted = super::format_atom(&atom);
764
765 assert_eq!(
766 source_format_normalized_alternative(&formatted, &mapping),
767 formatted
768 );
769 assert!(formatted.contains("\\\\"));
770 assert!(formatted.contains("\\n"));
771 assert!(formatted.contains("\\r"));
772 assert!(formatted.contains("\\t"));
773 assert!(formatted.contains("\\u{7}"));
774 assert!(!formatted.contains('\n'));
775 assert!(!formatted.contains('\r'));
776 assert!(!formatted.contains('\t'));
777 assert!(formatted.contains(&format!("\\\" {generated} \\\\\\n")));
778 }
779}