Skip to main content

module_lang/
error.rs

1//! The error type returned when the graph is built or a name is resolved.
2
3use core::fmt;
4
5use symbol_lang::Symbol;
6
7use crate::id::ModuleId;
8
9/// The reason a graph operation or a name resolution did not succeed.
10///
11/// Building the graph and resolving through it can fail five ways, each a
12/// distinct, defined outcome rather than a panic or a silently wrong answer:
13///
14/// - the id passed in was not minted by this graph
15///   ([`UnknownModule`](Self::UnknownModule)),
16/// - a name was declared twice in one module
17///   ([`DuplicateName`](Self::DuplicateName)),
18/// - a name is not declared in the module it was looked up in
19///   ([`Unresolved`](Self::Unresolved)),
20/// - an import targets a name that is private to the module that declares it
21///   ([`Private`](Self::Private)), or
22/// - an import chain loops back on itself ([`ImportCycle`](Self::ImportCycle)).
23///
24/// Each variant carries the [`ModuleId`] and [`Symbol`] it concerns, so a caller
25/// can recover the human-readable spelling from its own interner and build a
26/// richer diagnostic. The [`fmt::Display`] form is a terse fallback that names the
27/// raw ids — module-lang does not own the interner, so it cannot print the name.
28///
29/// The enum is `#[non_exhaustive]`: a downstream `match` must include a wildcard
30/// arm, so later additions never force a breaking change on callers.
31///
32/// # Examples
33///
34/// ```
35/// use intern_lang::Interner;
36/// use module_lang::{ModuleGraph, ResolveError, Visibility};
37/// use source_lang::SourceMap;
38///
39/// let mut sources = SourceMap::new();
40/// let mut names = Interner::new();
41/// let mut graph: ModuleGraph<()> = ModuleGraph::new();
42///
43/// let m = graph.add_module(names.intern("m"), sources.add("m", "").expect("fits"));
44/// let x = names.intern("x");
45/// graph.define(m, x, Visibility::Public, ()).expect("first definition is unique");
46///
47/// // Declaring the same name again in the same module is a defined error.
48/// assert!(matches!(
49///     graph.define(m, x, Visibility::Public, ()),
50///     Err(ResolveError::DuplicateName { .. }),
51/// ));
52///
53/// // Looking up a name that was never declared is a defined error, not a panic.
54/// let y = names.intern("y");
55/// assert!(matches!(
56///     graph.resolve(m, y),
57///     Err(ResolveError::Unresolved { .. }),
58/// ));
59/// ```
60#[derive(Clone, Debug, PartialEq, Eq)]
61#[non_exhaustive]
62pub enum ResolveError {
63    /// A [`ModuleId`] was passed that this graph never minted.
64    ///
65    /// Every id comes from
66    /// [`ModuleGraph::add_module`](crate::ModuleGraph::add_module) on the graph it
67    /// is used with; an id from a different graph, or one fabricated by other
68    /// means, lands here instead of indexing out of bounds.
69    UnknownModule(ModuleId),
70
71    /// A name was declared twice in the same module.
72    ///
73    /// Both [`define`](crate::ModuleGraph::define) and
74    /// [`import`](crate::ModuleGraph::import) reject a name already present in the
75    /// target module — a module's namespace is flat, so a definition and an import
76    /// cannot share a spelling. The conflict is reported where the second
77    /// declaration is added, not silently resolved last-write-wins.
78    DuplicateName {
79        /// The module the duplicate was added to.
80        module: ModuleId,
81        /// The name that was already declared there.
82        name: Symbol,
83    },
84
85    /// A name is not declared in the module it was looked up in.
86    ///
87    /// Returned by [`resolve`](crate::ModuleGraph::resolve) when neither a
88    /// definition nor an import in the module (or, following an import chain, in a
89    /// source module) declares the name.
90    Unresolved {
91        /// The module the name was looked up in.
92        module: ModuleId,
93        /// The name that could not be found.
94        name: Symbol,
95    },
96
97    /// An import resolved to a name that is private to the module declaring it.
98    ///
99    /// A [`Visibility::Private`](crate::Visibility::Private) definition is visible
100    /// only from within its own module; reaching it through an import from another
101    /// module is rejected here rather than silently allowed.
102    Private {
103        /// The module that declares the name privately.
104        module: ModuleId,
105        /// The private name an import tried to reach.
106        name: Symbol,
107    },
108
109    /// An import chain loops back on itself.
110    ///
111    /// Resolving an import follows it to its source module, and so on; if that walk
112    /// returns to a `(module, name)` pair it has already visited, the chain forms a
113    /// cycle. It is reported here in bounded time rather than recursing without
114    /// limit.
115    ImportCycle {
116        /// The module the walk looped back to.
117        module: ModuleId,
118        /// The name whose import chain forms the cycle.
119        name: Symbol,
120    },
121}
122
123impl fmt::Display for ResolveError {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        match self {
126            Self::UnknownModule(id) => {
127                write!(f, "module id {} was not minted by this graph", id.to_u32())
128            }
129            Self::DuplicateName { module, name } => write!(
130                f,
131                "name (symbol {}) is already declared in module {}",
132                name.as_u32(),
133                module.to_u32(),
134            ),
135            Self::Unresolved { module, name } => write!(
136                f,
137                "no name (symbol {}) is declared in module {}",
138                name.as_u32(),
139                module.to_u32(),
140            ),
141            Self::Private { module, name } => write!(
142                f,
143                "name (symbol {}) in module {} is private and cannot be imported",
144                name.as_u32(),
145                module.to_u32(),
146            ),
147            Self::ImportCycle { module, name } => write!(
148                f,
149                "import of name (symbol {}) forms a cycle at module {}",
150                name.as_u32(),
151                module.to_u32(),
152            ),
153        }
154    }
155}
156
157impl core::error::Error for ResolveError {}
158
159#[cfg(test)]
160#[allow(
161    clippy::expect_used,
162    reason = "unwrapping a known-good setup is fine in tests"
163)]
164mod tests {
165    extern crate alloc;
166    use alloc::string::ToString;
167
168    use super::*;
169
170    #[test]
171    fn test_unknown_module_display_names_the_id() {
172        let err = ResolveError::UnknownModule(ModuleId::from_index(4));
173        assert!(err.to_string().contains('4'), "{err}");
174    }
175
176    #[test]
177    fn test_duplicate_display_names_module_and_symbol() {
178        let err = ResolveError::DuplicateName {
179            module: ModuleId::from_index(2),
180            name: Symbol::from_u32(7).expect("nonzero"),
181        };
182        let text = err.to_string();
183        assert!(text.contains('2'), "{text}");
184        assert!(text.contains('7'), "{text}");
185    }
186
187    #[test]
188    fn test_error_is_clonable_and_equatable() {
189        let a = ResolveError::Unresolved {
190            module: ModuleId::from_index(1),
191            name: Symbol::from_u32(1).expect("nonzero"),
192        };
193        let b = a.clone();
194        assert_eq!(a, b);
195    }
196}