midenc_hir/asm/
import.rs

1use core::{
2    fmt::Write,
3    hash::{Hash, Hasher},
4    str::FromStr,
5};
6
7use anyhow::bail;
8use rustc_hash::{FxHashMap, FxHashSet};
9
10use crate::{
11    diagnostics::{SourceSpan, Spanned},
12    FunctionIdent, Ident, Symbol,
13};
14
15#[derive(Default, Debug, Clone)]
16pub struct ModuleImportInfo {
17    /// This maps original, fully-qualified module names to their corresponding import
18    modules: FxHashMap<Ident, MasmImport>,
19    /// This maps known aliases to their fully-qualified identifiers
20    aliases: FxHashMap<Ident, Ident>,
21    /// This maps short-form/aliased module names to the functions imported from that module
22    functions: FxHashMap<Ident, FxHashSet<FunctionIdent>>,
23}
24impl ModuleImportInfo {
25    /// Inserts a new import in the table
26    pub fn insert(&mut self, import: MasmImport) {
27        let name = Ident::new(import.name, import.span);
28        assert!(self.modules.insert(name, import).is_none());
29    }
30
31    /// Adds an import of the given function to the import table
32    ///
33    /// NOTE: It is assumed that the caller is adding imports using fully-qualified names.
34    pub fn add(&mut self, id: FunctionIdent) {
35        use std::collections::hash_map::Entry;
36
37        let module_id = id.module;
38        match self.modules.entry(module_id) {
39            Entry::Vacant(entry) => {
40                let alias = module_id_alias(module_id);
41                let span = module_id.span();
42                let alias_id = if self.aliases.contains_key(&alias) {
43                    // The alias is already used by another module, we must
44                    // produce a new, unique alias to avoid conflicts. We
45                    // use the hash of the fully-qualified name for this
46                    // purpose, hex-encoded
47                    let mut hasher = rustc_hash::FxHasher::default();
48                    alias.as_str().hash(&mut hasher);
49                    let mut buf = String::with_capacity(16);
50                    write!(&mut buf, "{:x}", hasher.finish()).expect("failed to write string");
51                    let alias = Symbol::intern(buf.as_str());
52                    let alias_id = Ident::new(alias, span);
53                    assert_eq!(
54                        self.aliases.insert(alias_id, module_id),
55                        None,
56                        "unexpected aliasing conflict"
57                    );
58                    alias_id
59                } else {
60                    Ident::new(alias, span)
61                };
62                entry.insert(MasmImport {
63                    span,
64                    name: module_id.as_symbol(),
65                    alias: alias_id.name,
66                });
67                self.aliases.insert(alias_id, module_id);
68                self.functions.entry(alias_id).or_default().insert(id);
69            }
70            Entry::Occupied(_) => {
71                let module_id_alias = module_id_alias(module_id);
72                let alias = self.aliases[&module_id_alias];
73                let functions = self.functions.entry(alias).or_default();
74                functions.insert(id);
75            }
76        }
77    }
78
79    /// Returns true if there are no imports recorded
80    pub fn is_empty(&self) -> bool {
81        self.modules.is_empty()
82    }
83
84    /// Given a fully-qualified module name, look up the corresponding import metadata
85    pub fn get<Q>(&self, module: &Q) -> Option<&MasmImport>
86    where
87        Ident: core::borrow::Borrow<Q>,
88        Q: Hash + Eq + ?Sized,
89    {
90        self.modules.get(module)
91    }
92
93    /// Given a fully-qualified module name, get the aliased identifier
94    pub fn alias<Q>(&self, module: &Q) -> Option<Ident>
95    where
96        Ident: core::borrow::Borrow<Q>,
97        Q: Hash + Eq + ?Sized,
98    {
99        self.modules.get(module).map(|i| Ident::new(i.alias, i.span))
100    }
101
102    /// Given an aliased module name, get the fully-qualified identifier
103    pub fn unalias<Q>(&self, alias: &Q) -> Option<Ident>
104    where
105        Ident: core::borrow::Borrow<Q>,
106        Q: Hash + Eq + ?Sized,
107    {
108        self.aliases.get(alias).copied()
109    }
110
111    /// Returns true if `module` is an imported module
112    pub fn is_import<Q>(&self, module: &Q) -> bool
113    where
114        Ident: core::borrow::Borrow<Q>,
115        Q: Hash + Eq + ?Sized,
116    {
117        self.modules.contains_key(module)
118    }
119
120    /// Given a module alias, get the set of functions imported from that module
121    pub fn imported<Q>(&self, alias: &Q) -> Option<&FxHashSet<FunctionIdent>>
122    where
123        Ident: core::borrow::Borrow<Q>,
124        Q: Hash + Eq + ?Sized,
125    {
126        self.functions.get(alias)
127    }
128
129    /// Get an iterator over the [MasmImport] records in this table
130    pub fn iter(&self) -> impl Iterator<Item = &MasmImport> {
131        self.modules.values()
132    }
133
134    /// Get an iterator over the aliased module names in this table
135    pub fn iter_module_names(&self) -> impl Iterator<Item = &Ident> {
136        self.modules.keys()
137    }
138}
139
140fn module_id_alias(module_id: Ident) -> Symbol {
141    match module_id.as_str().rsplit_once("::") {
142        None => module_id.as_symbol(),
143        Some((_, alias)) => Symbol::intern(alias),
144    }
145}
146
147/// This represents an import statement in Miden Assembly
148#[derive(Debug, Copy, Clone, Spanned)]
149pub struct MasmImport {
150    /// The source span corresponding to this import statement, if applicable
151    #[span]
152    pub span: SourceSpan,
153    /// The fully-qualified name of the imported module, e.g. `std::math::u64`
154    pub name: Symbol,
155    /// The name to which the imported module is aliased locally, e.g. `u64`
156    /// is the alias for `use std::math::u64`, which is the default behavior.
157    ///
158    /// However, custom aliases are permitted, and we may use this to disambiguate
159    /// imported modules, e.g. `use std::math::u64->my_u64` will result in the
160    /// alias for this import being `my_u64`.
161    pub alias: Symbol,
162}
163impl MasmImport {
164    /// Returns true if this import has a custom alias, or if it uses the
165    /// default aliasing behavior for imports
166    pub fn is_aliased(&self) -> bool {
167        !self.name.as_str().ends_with(self.alias.as_str())
168    }
169
170    /// Returns true if this import conflicts with `other`
171    ///
172    /// A conflict arises when the same name is used to reference two different
173    /// imports locally within a module, i.e. the aliases conflict
174    pub fn conflicts_with(&self, other: &Self) -> bool {
175        self.alias == other.alias && self.name != other.name
176    }
177}
178impl Eq for MasmImport {}
179impl PartialEq for MasmImport {
180    fn eq(&self, other: &Self) -> bool {
181        // If the names are different, the imports can't be equivalent
182        if self.name != other.name {
183            return false;
184        }
185        // Otherwise, equivalence depends on the aliasing of the import
186        match (self.is_aliased(), other.is_aliased()) {
187            (true, true) => {
188                // Two imports that are custom aliased are equivalent only if
189                // both the fully-qualified name and the alias are identical
190                self.alias == other.alias
191            }
192            (true, false) | (false, true) => {
193                // If one import is aliased and the other is not, the imports
194                // are never equivalent, because they can't possibly refer to
195                // the same module by the same name
196                false
197            }
198            (false, false) => {
199                // Two unaliased imports are the same if their names are the same
200                true
201            }
202        }
203    }
204}
205impl PartialOrd for MasmImport {
206    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
207        Some(self.cmp(other))
208    }
209}
210impl Ord for MasmImport {
211    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
212        self.name.cmp(&other.name).then_with(|| self.alias.cmp(&other.alias))
213    }
214}
215impl Hash for MasmImport {
216    fn hash<H: Hasher>(&self, state: &mut H) {
217        self.name.hash(state);
218        self.alias.hash(state);
219    }
220}
221impl TryFrom<Ident> for MasmImport {
222    type Error = anyhow::Error;
223
224    fn try_from(module: Ident) -> Result<Self, Self::Error> {
225        let name = module.as_str();
226        if name.contains(char::is_whitespace) {
227            bail!("invalid module identifier '{name}': cannot contain whitespace",);
228        }
229        match name.rsplit_once("::") {
230            None => {
231                let name = module.as_symbol();
232                Ok(Self {
233                    span: module.span(),
234                    name,
235                    alias: name,
236                })
237            }
238            Some((_, "")) => {
239                bail!("invalid module identifier '{name}': trailing '::' is invalid");
240            }
241            Some((_, alias)) => {
242                let name = module.as_symbol();
243                let alias = Symbol::intern(alias);
244                Ok(Self {
245                    span: module.span(),
246                    name,
247                    alias,
248                })
249            }
250        }
251    }
252}
253impl FromStr for MasmImport {
254    type Err = anyhow::Error;
255
256    /// Parse an import statement as seen in Miden Assembly, e.g. `use std::math::u64->bigint`
257    fn from_str(s: &str) -> Result<Self, Self::Err> {
258        let s = s.trim();
259        let s = s.strip_prefix("use ").unwrap_or(s);
260        match s.rsplit_once("->") {
261            None => {
262                let name = Ident::with_empty_span(Symbol::intern(s));
263                name.try_into()
264            }
265            Some((_, "")) => {
266                bail!("invalid import '{s}': alias cannot be empty")
267            }
268            Some((fqn, alias)) => {
269                let name = Symbol::intern(fqn);
270                let alias = Symbol::intern(alias);
271                Ok(Self {
272                    span: SourceSpan::UNKNOWN,
273                    name,
274                    alias,
275                })
276            }
277        }
278    }
279}