Skip to main content

xlog_logic/
diagnostics.rs

1//! Stable diagnostic records for rule provenance and query proof traces.
2
3use 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/// Origin class for a rule visible through diagnostic introspection.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum RuleSourceKind {
13    /// Authored in the parsed XLOG source.
14    Source,
15    /// Generated by a compiler or rewrite pass.
16    Generated,
17    /// Mined or induced by an external rule search path.
18    Mined,
19    /// Imported from a module boundary.
20    Imported,
21    /// Injected into a live runtime session.
22    RuntimeInjected,
23}
24
25impl RuleSourceKind {
26    /// Stable lowercase string used by CLI and pyxlog JSON/dict outputs.
27    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/// Public rule provenance record.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct RuleProvenance {
41    /// Stable rule identifier within the diagnostic report.
42    pub rule_id: String,
43    /// Formatted rule head, e.g. `reach(X, Y)`.
44    pub head: String,
45    /// Rule origin class.
46    pub source_kind: RuleSourceKind,
47    /// Source span when available from the parser.
48    pub source_span: Option<String>,
49    /// Stable hash of the source or generation trace.
50    pub generation_trace_hash: Option<String>,
51    /// Body relation ids that support this rule.
52    pub support_relation_ids: Vec<String>,
53    /// Counterexample relation ids when supplied by an induction path.
54    pub counterexample_relation_ids: Vec<String>,
55}
56
57/// Public query proof trace record.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct QueryProofTrace {
60    /// Stable query identifier.
61    pub query_id: String,
62    /// Formatted source query atom.
63    pub query: String,
64    /// Internal answer relation for the query.
65    pub answer_relation: String,
66    /// Rule ids that can derive the query predicate.
67    pub rule_ids: Vec<String>,
68    /// Source facts named by the direct proof frontier.
69    pub source_facts: Vec<String>,
70    /// Rejected or negated alternatives encountered in the trace.
71    pub rejected_alternatives: Vec<String>,
72}
73
74/// Build provenance for source rules and optional generated rewrite rules.
75pub 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
101/// Build source and generated rule-provenance records for a program.
102pub 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
121/// Build direct proof traces for source queries.
122pub 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
172/// Build coarse proof traces that name source rules and source facts per query.
173pub fn build_query_proof_traces(program: &Program) -> Vec<QueryProofTrace> {
174    let provenance = rule_provenance(program, None);
175    query_proof_traces(program, &provenance)
176}
177
178/// Build source-facing provenance and proof traces from authored and normalized programs.
179///
180/// Normalized support relations and rejected alternatives are merged only while rule and
181/// query order still align with the authored program. Compiler-generated predicates and
182/// variables are removed or mapped back to their authored function-local names.
183pub 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
271/// Map generated predicate-function variables to their authored local names.
272pub 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
344/// Replace generated predicate-function variable tokens with authored local names.
345pub 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
457/// Format an atom in source-like syntax.
458pub 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
468/// Format an integrity-constraint body in source-like syntax.
469pub 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
514/// Format a term in source-like syntax.
515pub 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}