Skip to main content

mangle_analysis/
name_trie.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
15//! Name trie for Mangle's name constant hierarchy.
16//!
17//! Maps name constants (like `/animal/dog`) to their most precise type
18//! via longest-prefix matching. Used by the bounds checker to infer types
19//! for name constants appearing in facts.
20
21use mangle_ir::{Inst, InstId, Ir};
22use rustc_hash::FxHashMap;
23
24use crate::type_expr;
25
26/// A trie over the `/`-separated segments of Mangle name constants.
27#[derive(Debug, Default)]
28pub struct NameTrie {
29    children: FxHashMap<String, NameTrie>,
30    is_terminal: bool,
31}
32
33impl NameTrie {
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Inserts a name path (e.g. "/foo/bar") into the trie.
39    pub fn add(&mut self, name: &str) {
40        let parts = split_name(name);
41        let mut node = self;
42        for part in parts {
43            node = node.children.entry(part.to_string()).or_default();
44        }
45        node.is_terminal = true;
46    }
47
48    /// Returns true if the exact name is in the trie.
49    pub fn contains(&self, name: &str) -> bool {
50        let parts = split_name(name);
51        let mut node = self;
52        for part in parts {
53            match node.children.get(part) {
54                Some(child) => node = child,
55                None => return false,
56            }
57        }
58        node.is_terminal
59    }
60
61    /// Finds the longest prefix of `name` that exists in the trie.
62    /// Returns the prefix as a name string, or `/name` if no prefix found.
63    ///
64    /// Example: if trie contains `/animal` and `/animal/dog`, and we look up
65    /// `/animal/dog/poodle`, returns `/animal/dog`.
66    pub fn prefix_name(&self, name: &str) -> String {
67        let parts = split_name(name);
68        let mut node = self;
69        let mut last_terminal_idx: Option<usize> = None;
70
71        for (i, part) in parts.iter().enumerate() {
72            match node.children.get(*part) {
73                Some(child) => {
74                    if child.is_terminal {
75                        last_terminal_idx = Some(i);
76                    }
77                    node = child;
78                }
79                None => break,
80            }
81        }
82
83        match last_terminal_idx {
84            Some(idx) => {
85                let prefix_parts = &parts[..=idx];
86                format!("/{}", prefix_parts.join("/"))
87            }
88            None => "/name".to_string(),
89        }
90    }
91
92    /// Collects all name constants from a type expression into this trie.
93    ///
94    /// Special handling for `fn:TaggedUnion`: skips the tag field (arg 0)
95    /// and tag values (odd-indexed args), only recurses into variant
96    /// struct types (even-indexed args from index 2).
97    pub fn collect(&mut self, ir: &Ir, id: InstId) {
98        match ir.get(id) {
99            Inst::Name(n) => {
100                let name = ir.resolve_name(*n);
101                if !type_expr::is_base_type(ir, id) && name.starts_with('/') {
102                    self.add(name);
103                }
104            }
105            Inst::ApplyFn { function, args } => {
106                let fname = ir.resolve_name(*function);
107                if fname == type_expr::FN_TAGGED_UNION && args.len() >= 3 {
108                    // Only collect from variant struct types, not tag
109                    // field name or variant tag values.
110                    for i in (2..args.len()).step_by(2) {
111                        self.collect(ir, args[i]);
112                    }
113                } else {
114                    for arg in args {
115                        self.collect(ir, *arg);
116                    }
117                }
118            }
119            _ => {}
120        }
121    }
122}
123
124/// Splits a name like "/foo/bar" into segments ["foo", "bar"].
125fn split_name(name: &str) -> Vec<&str> {
126    name.split('/').filter(|s| !s.is_empty()).collect()
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn basic_trie_operations() {
135        let mut trie = NameTrie::new();
136        trie.add("/animal");
137        trie.add("/animal/dog");
138        trie.add("/color");
139
140        assert!(trie.contains("/animal"));
141        assert!(trie.contains("/animal/dog"));
142        assert!(trie.contains("/color"));
143        assert!(!trie.contains("/animal/cat"));
144        assert!(!trie.contains("/plant"));
145    }
146
147    #[test]
148    fn prefix_name_lookup() {
149        let mut trie = NameTrie::new();
150        trie.add("/animal");
151        trie.add("/animal/dog");
152
153        assert_eq!(trie.prefix_name("/animal/dog/poodle"), "/animal/dog");
154        assert_eq!(trie.prefix_name("/animal/cat"), "/animal");
155        assert_eq!(trie.prefix_name("/animal"), "/animal");
156        assert_eq!(trie.prefix_name("/plant/rose"), "/name");
157    }
158
159    #[test]
160    fn collect_from_type_expr() {
161        let mut ir = Ir::new();
162        let mut trie = NameTrie::new();
163
164        // Build: fn:Struct(/x, /animal, /y, /color)
165        let x = {
166            let n = ir.intern_name("/x");
167            ir.add_inst(Inst::Name(n))
168        };
169        let animal = {
170            let n = ir.intern_name("/animal");
171            ir.add_inst(Inst::Name(n))
172        };
173        let y = {
174            let n = ir.intern_name("/y");
175            ir.add_inst(Inst::Name(n))
176        };
177        let color = {
178            let n = ir.intern_name("/color");
179            ir.add_inst(Inst::Name(n))
180        };
181        let struct_name = ir.intern_name("fn:Struct");
182        let struct_type = ir.add_inst(Inst::ApplyFn {
183            function: struct_name,
184            args: vec![x, animal, y, color],
185        });
186
187        trie.collect(&ir, struct_type);
188        assert!(trie.contains("/x"));
189        assert!(trie.contains("/animal"));
190        assert!(trie.contains("/y"));
191        assert!(trie.contains("/color"));
192    }
193
194    #[test]
195    fn collect_from_tagged_union_skips_tags() {
196        let mut ir = Ir::new();
197        let mut trie = NameTrie::new();
198
199        // fn:TaggedUnion(/kind, /move, fn:Struct(/x, /number), /quit, fn:Struct())
200        let kind = {
201            let n = ir.intern_name("/kind");
202            ir.add_inst(Inst::Name(n))
203        };
204        let move_ = {
205            let n = ir.intern_name("/move");
206            ir.add_inst(Inst::Name(n))
207        };
208        let x = {
209            let n = ir.intern_name("/x");
210            ir.add_inst(Inst::Name(n))
211        };
212        let number = {
213            let n = ir.intern_name("/number");
214            ir.add_inst(Inst::Name(n))
215        };
216        let struct_name = ir.intern_name("fn:Struct");
217        let move_struct = ir.add_inst(Inst::ApplyFn {
218            function: struct_name,
219            args: vec![x, number],
220        });
221        let quit = {
222            let n = ir.intern_name("/quit");
223            ir.add_inst(Inst::Name(n))
224        };
225        let quit_struct = ir.add_inst(Inst::ApplyFn {
226            function: struct_name,
227            args: vec![],
228        });
229        let tu_name = ir.intern_name("fn:TaggedUnion");
230        let tu = ir.add_inst(Inst::ApplyFn {
231            function: tu_name,
232            args: vec![kind, move_, move_struct, quit, quit_struct],
233        });
234
235        trie.collect(&ir, tu);
236
237        // Should collect /x and /number from variant structs.
238        assert!(trie.contains("/x"));
239        // /number is a base type, so it should NOT be collected.
240        assert!(!trie.contains("/number"));
241        // Should NOT collect /kind (tag field), /move, /quit (tag values).
242        assert!(!trie.contains("/kind"));
243        assert!(!trie.contains("/move"));
244        assert!(!trie.contains("/quit"));
245    }
246}