Skip to main content

mangle_analysis/
rename.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use mangle_ast as ast;
16use rustc_hash::{FxHashMap, FxHashSet};
17
18/// Rewrites a unit by prefixing local predicates with the package name.
19pub fn rewrite_unit<'a>(arena: &'a ast::Arena, unit: &'a ast::Unit<'a>) -> ast::Unit<'a> {
20    let (pkg_name, used_pkgs) = find_package_info(arena, unit);
21
22    if pkg_name.is_empty() {
23        return ast::Unit {
24            decls: unit.decls,
25            clauses: unit.clauses,
26        };
27    }
28
29    let defined_preds = find_defined_preds(unit);
30    let mut renamer = Renamer {
31        arena,
32        pkg_name,
33        used_pkgs,
34        defined_preds,
35        cache: FxHashMap::default(),
36    };
37
38    let mut new_decls = Vec::with_capacity(unit.decls.len());
39    for &decl in unit.decls {
40        if let Some(new_decl) = renamer.rewrite_decl(decl) {
41            new_decls.push(new_decl);
42        }
43    }
44
45    let mut new_clauses = Vec::with_capacity(unit.clauses.len());
46    for &clause in unit.clauses {
47        if let Some(new_clause) = renamer.rewrite_clause(clause) {
48            new_clauses.push(new_clause);
49        }
50    }
51
52    ast::Unit {
53        decls: arena.alloc_slice_copy(&new_decls),
54        clauses: arena.alloc_slice_copy(&new_clauses),
55    }
56}
57
58fn find_package_info<'a>(
59    arena: &'a ast::Arena,
60    unit: &'a ast::Unit<'a>,
61) -> (&'a str, FxHashSet<&'a str>) {
62    let mut pkg_name = "";
63    let mut used_pkgs = FxHashSet::default();
64
65    for &decl in unit.decls {
66        let pred_name = arena.predicate_name(decl.atom.sym).unwrap_or("");
67        if pred_name == "Package" {
68            if let Some(desc) = find_name_desc(arena, decl.descr) {
69                pkg_name = desc;
70            }
71        } else if pred_name == "Use"
72            && let Some(desc) = find_name_desc(arena, decl.descr)
73        {
74            used_pkgs.insert(desc);
75        }
76    }
77    (pkg_name, used_pkgs)
78}
79
80fn find_name_desc<'a>(arena: &'a ast::Arena, descr: &'a [&'a ast::Atom<'a>]) -> Option<&'a str> {
81    for &atom in descr {
82        if arena.predicate_name(atom.sym).unwrap_or("") == "name"
83            && let Some(&ast::BaseTerm::Const(ast::Const::String(s))) = atom.args.first().copied()
84        {
85            return Some(s);
86        }
87    }
88    None
89}
90
91fn find_defined_preds(unit: &ast::Unit) -> FxHashSet<ast::PredicateIndex> {
92    let mut defined = FxHashSet::default();
93    for &decl in unit.decls {
94        defined.insert(decl.atom.sym);
95    }
96    for &clause in unit.clauses {
97        defined.insert(clause.head.sym);
98    }
99    defined
100}
101
102struct Renamer<'a> {
103    arena: &'a ast::Arena,
104    pkg_name: &'a str,
105    used_pkgs: FxHashSet<&'a str>,
106    defined_preds: FxHashSet<ast::PredicateIndex>,
107    cache: FxHashMap<ast::PredicateIndex, ast::PredicateIndex>,
108}
109
110impl<'a> Renamer<'a> {
111    fn rename_pred(&mut self, sym: ast::PredicateIndex) -> Option<ast::PredicateIndex> {
112        if let Some(&new_sym) = self.cache.get(&sym) {
113            return Some(new_sym);
114        }
115
116        let name = self.arena.predicate_name(sym)?;
117
118        // Don't rename Package and Use predicates themselves
119        if name == "Package" || name == "Use" {
120            self.cache.insert(sym, sym);
121            return Some(sym);
122        }
123
124        if self.defined_preds.contains(&sym) {
125            let new_name = format!("{}.{}", self.pkg_name, name);
126            let new_sym = self
127                .arena
128                .predicate_sym(&new_name, self.arena.predicate_arity(sym));
129            self.cache.insert(sym, new_sym);
130            return Some(new_sym);
131        }
132
133        // Check for cross-package references (e.g. `other.foo`)
134        if let Some(dot_idx) = name.rfind('.') {
135            let prefix = &name[..dot_idx];
136            if self.used_pkgs.contains(prefix) {
137                // It's a valid external reference, keep it as is.
138                self.cache.insert(sym, sym);
139                return Some(sym);
140            }
141        }
142
143        // Not defined locally, no dot. Must be a builtin or global?
144        self.cache.insert(sym, sym);
145        Some(sym)
146    }
147
148    fn rewrite_decl(&mut self, decl: &'a ast::Decl<'a>) -> Option<&'a ast::Decl<'a>> {
149        let pred_name = self.arena.predicate_name(decl.atom.sym).unwrap_or("");
150        if pred_name == "Package" || pred_name == "Use" {
151            // Remove Package and Use declarations from the rewritten unit
152            return None;
153        }
154
155        let new_atom = self.rewrite_atom(decl.atom)?;
156
157        // Rewrite bounds if necessary
158        let bounds = if let Some(bs) = decl.bounds {
159            let mut new_bounds = Vec::new();
160            for &b in bs {
161                let new_base_terms: Vec<&ast::BaseTerm> = b
162                    .base_terms
163                    .iter()
164                    .map(|&t| {
165                        // Check if it's a name constant that needs rewriting (e.g. type names like /foo -> /pkg.foo)
166                        if let ast::BaseTerm::Const(ast::Const::Name(name_idx)) = t {
167                            let name = self.arena.lookup_name(*name_idx).unwrap_or("");
168                            // Check if name corresponds to a defined predicate
169                            if let Some(pred_idx) = self.arena.lookup_predicate_sym(*name_idx)
170                                && self.defined_preds.contains(&pred_idx)
171                            {
172                                let new_name = format!("{}.{}", self.pkg_name, name);
173                                let new_name_idx = self.arena.intern(&new_name);
174                                return &*self
175                                    .arena
176                                    .alloc(ast::BaseTerm::Const(ast::Const::Name(new_name_idx)));
177                            }
178                        }
179                        t
180                    })
181                    .collect();
182                new_bounds.push(&*self.arena.alloc(ast::BoundDecl {
183                    base_terms: self.arena.alloc_slice_copy(&new_base_terms),
184                }));
185            }
186            Some(self.arena.alloc_slice_copy(&new_bounds) as &'a [&'a ast::BoundDecl<'a>])
187        } else {
188            None
189        };
190
191        Some(self.arena.alloc(ast::Decl {
192            atom: new_atom,
193            descr: decl.descr, // Descriptions usually don't contain predicates to rename?
194            bounds,
195            constraints: decl.constraints,
196            is_temporal: decl.is_temporal,
197        }))
198    }
199
200    fn rewrite_clause(&mut self, clause: &'a ast::Clause<'a>) -> Option<&'a ast::Clause<'a>> {
201        let head = self.rewrite_atom(clause.head)?;
202        let mut premises = Vec::new();
203        for &premise in clause.premises {
204            match premise {
205                ast::Term::Atom(a) => {
206                    premises.push(&*self.arena.alloc(ast::Term::Atom(self.rewrite_atom(a)?)));
207                }
208                ast::Term::NegAtom(a) => {
209                    premises.push(&*self.arena.alloc(ast::Term::NegAtom(self.rewrite_atom(a)?)));
210                }
211                ast::Term::TemporalAtom(a, interval) => {
212                    premises.push(
213                        &*self
214                            .arena
215                            .alloc(ast::Term::TemporalAtom(self.rewrite_atom(a)?, *interval)),
216                    );
217                }
218                _ => premises.push(premise),
219            }
220        }
221
222        Some(self.arena.alloc(ast::Clause {
223            head,
224            head_time: clause.head_time,
225            premises: self.arena.alloc_slice_copy(&premises),
226            transform: clause.transform,
227        }))
228    }
229
230    fn rewrite_atom(&mut self, atom: &'a ast::Atom<'a>) -> Option<&'a ast::Atom<'a>> {
231        let new_sym = self.rename_pred(atom.sym)?;
232        Some(self.arena.atom(new_sym, atom.args))
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use googletest::prelude::*;
240
241    #[test]
242    fn test_rename_simple() {
243        let arena = ast::Arena::new_with_global_interner();
244
245        // Package pkg!
246        // foo(X) :- bar(X).
247        // bar(1).
248
249        let pkg_sym = arena.predicate_sym("Package", Some(0));
250        let name_sym = arena.predicate_sym("name", Some(1));
251        let pkg_name = arena.alloc(ast::BaseTerm::Const(ast::Const::String("pkg")));
252        let pkg_decl = arena.alloc(ast::Decl {
253            atom: arena.atom(pkg_sym, &[]),
254            descr: arena.alloc_slice_copy(&[arena.atom(name_sym, &[pkg_name])]),
255            bounds: None,
256            constraints: None,
257            is_temporal: false,
258        });
259
260        let foo_sym = arena.predicate_sym("foo", Some(1));
261        let bar_sym = arena.predicate_sym("bar", Some(1));
262        let var_x = arena.variable("X");
263        let const_1 = arena.const_(ast::Const::Number(1));
264
265        let clause1 = arena.alloc(ast::Clause {
266            head: arena.atom(foo_sym, &[var_x]),
267            head_time: None,
268            premises: arena
269                .alloc_slice_copy(&[arena.alloc(ast::Term::Atom(arena.atom(bar_sym, &[var_x])))]),
270            transform: &[],
271        });
272
273        let clause2 = arena.alloc(ast::Clause {
274            head: arena.atom(bar_sym, &[const_1]),
275            head_time: None,
276            premises: &[],
277            transform: &[],
278        });
279
280        let unit = ast::Unit {
281            decls: arena.alloc_slice_copy(&[pkg_decl]),
282            clauses: arena.alloc_slice_copy(&[clause1, clause2]),
283        };
284
285        let new_unit = rewrite_unit(&arena, &unit);
286
287        // Package decl should be removed
288        assert_that!(new_unit.decls.len(), eq(0));
289        assert_that!(new_unit.clauses.len(), eq(2));
290
291        let c1 = new_unit.clauses[0];
292        let c2 = new_unit.clauses[1];
293
294        // Check head of c1: foo -> pkg.foo
295        let head_name = arena.predicate_name(c1.head.sym).unwrap();
296        assert_that!(head_name, eq("pkg.foo"));
297
298        // Check premise of c1: bar -> pkg.bar
299        if let ast::Term::Atom(a) = c1.premises[0] {
300            let p_name = arena.predicate_name(a.sym).unwrap();
301            assert_that!(p_name, eq("pkg.bar"));
302        } else {
303            panic!("Expected Atom premise");
304        }
305
306        // Check head of c2: bar -> pkg.bar
307        let head_name_2 = arena.predicate_name(c2.head.sym).unwrap();
308        assert_that!(head_name_2, eq("pkg.bar"));
309    }
310
311    #[test]
312    fn test_rename_with_use() {
313        let arena = ast::Arena::new_with_global_interner();
314
315        // Package pkg!
316        // Use other!
317        // foo(X) :- other.bar(X).
318
319        let pkg_sym = arena.predicate_sym("Package", Some(0));
320        let use_sym = arena.predicate_sym("Use", Some(0));
321        let name_sym = arena.predicate_sym("name", Some(1));
322
323        let pkg_name = arena.alloc(ast::BaseTerm::Const(ast::Const::String("pkg")));
324        let pkg_decl = arena.alloc(ast::Decl {
325            atom: arena.atom(pkg_sym, &[]),
326            descr: arena.alloc_slice_copy(&[arena.atom(name_sym, &[pkg_name])]),
327            bounds: None,
328            constraints: None,
329            is_temporal: false,
330        });
331
332        let other_name = arena.alloc(ast::BaseTerm::Const(ast::Const::String("other")));
333        let use_decl = arena.alloc(ast::Decl {
334            atom: arena.atom(use_sym, &[]),
335            descr: arena.alloc_slice_copy(&[arena.atom(name_sym, &[other_name])]),
336            bounds: None,
337            constraints: None,
338            is_temporal: false,
339        });
340
341        let foo_sym = arena.predicate_sym("foo", Some(1));
342        let other_bar_sym = arena.predicate_sym("other.bar", Some(1));
343        let var_x = arena.variable("X");
344
345        let clause1 = arena.alloc(ast::Clause {
346            head: arena.atom(foo_sym, &[var_x]),
347            head_time: None,
348            premises: arena.alloc_slice_copy(&[
349                arena.alloc(ast::Term::Atom(arena.atom(other_bar_sym, &[var_x])))
350            ]),
351            transform: &[],
352        });
353
354        let unit = ast::Unit {
355            decls: arena.alloc_slice_copy(&[pkg_decl, use_decl]),
356            clauses: arena.alloc_slice_copy(&[clause1]),
357        };
358
359        let new_unit = rewrite_unit(&arena, &unit);
360
361        assert_that!(new_unit.clauses.len(), eq(1));
362        let c1 = new_unit.clauses[0];
363
364        // foo -> pkg.foo
365        let head_name = arena.predicate_name(c1.head.sym).unwrap();
366        assert_that!(head_name, eq("pkg.foo"));
367
368        // other.bar -> other.bar (unchanged because 'other' is Used)
369        if let ast::Term::Atom(a) = c1.premises[0] {
370            let p_name = arena.predicate_name(a.sym).unwrap();
371            assert_that!(p_name, eq("other.bar"));
372        } else {
373            panic!("Expected Atom premise");
374        }
375    }
376
377    fn make_pkg_decl<'a>(arena: &'a ast::Arena, name: &str) -> &'a ast::Decl<'a> {
378        let pkg_sym = arena.predicate_sym("Package", Some(0));
379        let name_sym = arena.predicate_sym("name", Some(1));
380        let pkg_name = arena.alloc(ast::BaseTerm::Const(ast::Const::String(
381            arena.alloc_str(name),
382        )));
383        arena.alloc(ast::Decl {
384            atom: arena.atom(pkg_sym, &[]),
385            descr: arena.alloc_slice_copy(&[arena.atom(name_sym, &[pkg_name])]),
386            bounds: None,
387            constraints: None,
388            is_temporal: false,
389        })
390    }
391
392    #[test]
393    fn test_go_case_no_package() {
394        // "no package name, clauses are not rewritten"
395        let arena = ast::Arena::new_with_global_interner();
396        let clause = arena.alloc(ast::Clause {
397            head: arena.atom(arena.predicate_sym("clause_defined_here", None), &[]),
398            head_time: None,
399            premises: arena.alloc_slice_copy(&[arena.alloc(ast::Term::Atom(
400                arena.atom(arena.predicate_sym("other_clause", None), &[]),
401            ))]),
402            transform: &[],
403        });
404        let unit = ast::Unit {
405            decls: &[],
406            clauses: arena.alloc_slice_copy(&[clause]),
407        };
408        let new_unit = rewrite_unit(&arena, &unit);
409        let head = arena.predicate_name(new_unit.clauses[0].head.sym).unwrap();
410        assert_that!(head, eq("clause_defined_here"));
411        if let ast::Term::Atom(a) = new_unit.clauses[0].premises[0] {
412            let p = arena.predicate_name(a.sym).unwrap();
413            assert_that!(p, eq("other_clause"));
414        }
415    }
416
417    #[test]
418    fn test_go_case_external_refs() {
419        // "references to predicates outside the package are left as-is"
420        let arena = ast::Arena::new_with_global_interner();
421        // Package foo.bar!
422        let pkg_decl = make_pkg_decl(&arena, "foo.bar");
423
424        // clause_defined_here :- other_clause.
425        // (other_clause is NOT defined locally)
426        let clause = arena.alloc(ast::Clause {
427            head: arena.atom(arena.predicate_sym("clause_defined_here", None), &[]),
428            head_time: None,
429            premises: arena.alloc_slice_copy(&[arena.alloc(ast::Term::Atom(
430                arena.atom(arena.predicate_sym("other_clause", None), &[]),
431            ))]),
432            transform: &[],
433        });
434
435        let unit = ast::Unit {
436            decls: arena.alloc_slice_copy(&[pkg_decl]),
437            clauses: arena.alloc_slice_copy(&[clause]),
438        };
439        let new_unit = rewrite_unit(&arena, &unit);
440
441        // head -> foo.bar.clause_defined_here
442        let head = arena.predicate_name(new_unit.clauses[0].head.sym).unwrap();
443        assert_that!(head, eq("foo.bar.clause_defined_here"));
444
445        // premise -> other_clause (unchanged)
446        if let ast::Term::Atom(a) = new_unit.clauses[0].premises[0] {
447            let p = arena.predicate_name(a.sym).unwrap();
448            assert_that!(p, eq("other_clause"));
449        }
450    }
451
452    #[test]
453    fn test_go_case_rewritten_local() {
454        // "clauses defined in this package are rewritten"
455        let arena = ast::Arena::new_with_global_interner();
456        let pkg_decl = make_pkg_decl(&arena, "foo.bar");
457
458        let defined_sym = arena.predicate_sym("clause_defined_here", None);
459        let other_sym = arena.predicate_sym("other_clause", None);
460
461        // other_clause().
462        let clause1 = arena.alloc(ast::Clause {
463            head: arena.atom(other_sym, &[]),
464            head_time: None,
465            premises: &[],
466            transform: &[],
467        });
468
469        // clause_defined_here() :- other_clause().
470        let clause2 = arena.alloc(ast::Clause {
471            head: arena.atom(defined_sym, &[]),
472            head_time: None,
473            premises: arena
474                .alloc_slice_copy(&[arena.alloc(ast::Term::Atom(arena.atom(other_sym, &[])))]),
475            transform: &[],
476        });
477
478        let unit = ast::Unit {
479            decls: arena.alloc_slice_copy(&[pkg_decl]),
480            clauses: arena.alloc_slice_copy(&[clause1, clause2]),
481        };
482        let new_unit = rewrite_unit(&arena, &unit);
483
484        // clause1 head: foo.bar.other_clause
485        let h1 = arena.predicate_name(new_unit.clauses[0].head.sym).unwrap();
486        assert_that!(h1, eq("foo.bar.other_clause"));
487
488        // clause2 head: foo.bar.clause_defined_here
489        let h2 = arena.predicate_name(new_unit.clauses[1].head.sym).unwrap();
490        assert_that!(h2, eq("foo.bar.clause_defined_here"));
491
492        // clause2 premise: foo.bar.other_clause
493        if let ast::Term::Atom(a) = new_unit.clauses[1].premises[0] {
494            let p = arena.predicate_name(a.sym).unwrap();
495            assert_that!(p, eq("foo.bar.other_clause"));
496        }
497    }
498
499    #[test]
500    fn test_go_case_negation() {
501        // "clause with a negation is rewritten"
502        let arena = ast::Arena::new_with_global_interner();
503        let pkg_decl = make_pkg_decl(&arena, "foo.bar");
504
505        let defined_sym = arena.predicate_sym("clause_defined_here", None);
506        let other_sym = arena.predicate_sym("other_clause", None);
507
508        // other_clause(). (Needs to be defined to trigger renaming)
509        let clause1 = arena.alloc(ast::Clause {
510            head: arena.atom(other_sym, &[]),
511            head_time: None,
512            premises: &[],
513            transform: &[],
514        });
515
516        // clause_defined_here() :- !other_clause().
517        let clause2 = arena.alloc(ast::Clause {
518            head: arena.atom(defined_sym, &[]),
519            head_time: None,
520            premises: arena
521                .alloc_slice_copy(&[arena.alloc(ast::Term::NegAtom(arena.atom(other_sym, &[])))]),
522            transform: &[],
523        });
524
525        let unit = ast::Unit {
526            decls: arena.alloc_slice_copy(&[pkg_decl]),
527            clauses: arena.alloc_slice_copy(&[clause1, clause2]),
528        };
529        let new_unit = rewrite_unit(&arena, &unit);
530
531        if let ast::Term::NegAtom(a) = new_unit.clauses[1].premises[0] {
532            let p = arena.predicate_name(a.sym).unwrap();
533            assert_that!(p, eq("foo.bar.other_clause"));
534        } else {
535            panic!("Expected NegAtom");
536        }
537    }
538
539    #[test]
540    fn test_go_case_decl_only() {
541        // "clauses are also rewritten if the decl was declared in this package"
542        let arena = ast::Arena::new_with_global_interner();
543        let pkg_decl = make_pkg_decl(&arena, "foo.bar");
544
545        let clause_sym = arena.predicate_sym("clause", None);
546        let decl_sym = arena.predicate_sym("from_decl", None);
547
548        // Decl from_decl.
549        let decl = arena.alloc(ast::Decl {
550            atom: arena.atom(decl_sym, &[]),
551            descr: &[],
552            bounds: None,
553            constraints: None,
554            is_temporal: false,
555        });
556
557        // clause() :- from_decl().
558        let clause = arena.alloc(ast::Clause {
559            head: arena.atom(clause_sym, &[]),
560            head_time: None,
561            premises: arena
562                .alloc_slice_copy(&[arena.alloc(ast::Term::Atom(arena.atom(decl_sym, &[])))]),
563            transform: &[],
564        });
565
566        let unit = ast::Unit {
567            decls: arena.alloc_slice_copy(&[pkg_decl, decl]),
568            clauses: arena.alloc_slice_copy(&[clause]),
569        };
570        let new_unit = rewrite_unit(&arena, &unit);
571
572        // clause -> foo.bar.clause
573        let h = arena.predicate_name(new_unit.clauses[0].head.sym).unwrap();
574        assert_that!(h, eq("foo.bar.clause"));
575
576        // from_decl -> foo.bar.from_decl
577        if let ast::Term::Atom(a) = new_unit.clauses[0].premises[0] {
578            let p = arena.predicate_name(a.sym).unwrap();
579            assert_that!(p, eq("foo.bar.from_decl"));
580        }
581
582        // Decl atom also rewritten
583        let d_name = arena.predicate_name(new_unit.decls[0].atom.sym).unwrap();
584        assert_that!(d_name, eq("foo.bar.from_decl"));
585    }
586}