leo_ast/common/path.rs
1// Copyright (C) 2019-2026 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use crate::{Canonicalize, Expression, Identifier, Location, Node, NodeID, ProgramId, simple_node_impl};
18
19use leo_span::{Span, Symbol, with_session_globals};
20
21impl Canonicalize for Path {
22 fn canonicalize(self) -> Self {
23 // Full destructure so adding a field triggers a compile error here.
24 let Self { user_program, qualifier, identifier, target, span: _, id: _ } = self;
25 let (user_program, qualifier) = match target {
26 PathTarget::Global(_) | PathTarget::Local(_) => (None, Vec::new()),
27 PathTarget::Unresolved => (user_program.canonicalize(), qualifier.canonicalize()),
28 };
29 Self {
30 user_program,
31 qualifier,
32 identifier: identifier.canonicalize(),
33 target,
34 span: Span::default(),
35 id: NodeID::default(),
36 }
37 }
38}
39
40use indexmap::IndexSet;
41use itertools::Itertools;
42use serde::Serialize;
43use std::{fmt, hash::Hash};
44
45/// A Path in a program.
46#[derive(Clone, Hash, Eq, PartialEq, Serialize)]
47pub struct Path {
48 /// The program this path belongs to, if set by the user
49 user_program: Option<ProgramId>,
50
51 /// The qualifying namespace segments written by the user, excluding the item itself.
52 /// e.g., in `foo::bar::baz`, this would be `[foo, bar]`.
53 qualifier: Vec<Identifier>,
54
55 /// The final item in the path, e.g., `baz` in `foo::bar::baz`.
56 identifier: Identifier,
57
58 /// The target type (i.e. local v.s. global) of this path.
59 target: PathTarget,
60
61 /// A span locating where the path occurred in the source.
62 pub span: Span,
63
64 /// The ID of the node.
65 pub id: NodeID,
66}
67
68#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize)]
69pub enum PathTarget {
70 Unresolved,
71 Local(Symbol),
72 Global(Location),
73}
74
75simple_node_impl!(Path);
76
77impl Path {
78 /// Creates a new unresolved `Path` from the given components.
79 ///
80 /// - `user_program`: An optional program name (e.g. `credits` in `credits.aleo::Bar`)
81 /// - `qualifier`: The namespace segments (e.g., `foo::bar` in `foo::bar::baz`).
82 /// - `identifier`: The final item in the path (e.g., `baz`).
83 /// - `span`: The source code span for this path.
84 /// - `id`: The node ID.
85 pub fn new(
86 user_program: Option<ProgramId>,
87 qualifier: Vec<Identifier>,
88 identifier: Identifier,
89 span: Span,
90 id: NodeID,
91 ) -> Self {
92 Self { user_program, qualifier, identifier, target: PathTarget::Unresolved, span, id }
93 }
94
95 /// Returns the final identifier of the path (e.g., `baz` in `foo::bar::baz`).
96 pub fn identifier(&self) -> &Identifier {
97 &self.identifier
98 }
99
100 /// Returns a slice of the qualifier segments (e.g., `[foo, bar]` in `foo::bar::baz`).
101 pub fn qualifier(&self) -> &[Identifier] {
102 &self.qualifier
103 }
104
105 /// Returns an iterator over all segments as `Symbol`s (qualifiers + identifier).
106 pub fn segments_iter(&self) -> impl Iterator<Item = Symbol> + '_ {
107 self.qualifier.iter().map(|id| id.name).chain(std::iter::once(self.identifier.name))
108 }
109
110 /// Returns a `Vec<Symbol>` of the segments.
111 pub fn segments(&self) -> Vec<Symbol> {
112 self.segments_iter().collect()
113 }
114
115 /// Returns the optional program identifier.
116 pub fn user_program(&self) -> Option<&ProgramId> {
117 self.user_program.as_ref()
118 }
119
120 /// Returns `self` after setting it `user_program` field to `user_program`.
121 pub fn with_user_program(mut self, user_program: ProgramId) -> Self {
122 self.user_program = Some(user_program);
123 self
124 }
125
126 pub fn span(&self) -> Span {
127 self.span
128 }
129
130 pub fn id(&self) -> NodeID {
131 self.id
132 }
133
134 pub fn is_resolved(&self) -> bool {
135 !matches!(self.target, PathTarget::Unresolved)
136 }
137
138 pub fn is_local(&self) -> bool {
139 matches!(self.target, PathTarget::Local(_))
140 }
141
142 pub fn is_global(&self) -> bool {
143 matches!(self.target, PathTarget::Global(_))
144 }
145
146 /// Returns the program symbol this path refers to, if known.
147 ///
148 /// Priority:
149 /// 1. User-written program qualifier (e.g. `foo.aleo::bar::baz`)
150 /// 2. Resolved global target program
151 /// 3. None (unresolved or local)
152 pub fn program(&self) -> Option<Symbol> {
153 if let Some(id) = &self.user_program {
154 return Some(id.as_symbol());
155 }
156
157 match &self.target {
158 PathTarget::Global(location) => Some(location.program),
159 _ => None,
160 }
161 }
162
163 /// Returns the `Symbol` if local, `None` if not.
164 pub fn try_local_symbol(&self) -> Option<Symbol> {
165 match self.target {
166 PathTarget::Local(sym) => Some(sym),
167 _ => None,
168 }
169 }
170
171 /// Returns the `Location` if global, `None` if not.
172 pub fn try_global_location(&self) -> Option<&Location> {
173 match &self.target {
174 PathTarget::Global(loc) => Some(loc),
175 _ => None,
176 }
177 }
178
179 /// Returns the `Symbol` if local, panics if not.
180 pub fn expect_local_symbol(&self) -> Symbol {
181 match self.target {
182 PathTarget::Local(sym) => sym,
183 _ => panic!("Expected a local path, found {:?}", self.target),
184 }
185 }
186
187 /// Returns the `Location` if global, panics if not.
188 pub fn expect_global_location(&self) -> &Location {
189 match &self.target {
190 PathTarget::Global(loc) => loc,
191 _ => panic!("Expected a global path, found {:?}", self.target),
192 }
193 }
194
195 /// Resolves this path to a local symbol.
196 pub fn to_local(self) -> Self {
197 Self { target: PathTarget::Local(self.identifier.name), ..self }
198 }
199
200 /// Resolves this path to a global location.
201 pub fn to_global(self, location: Location) -> Self {
202 Self { target: PathTarget::Global(location), ..self }
203 }
204
205 /// Returns a new `Path` with the final identifier replaced by `new_symbol`.
206 ///
207 /// This updates:
208 /// - `identifier.name`
209 /// - `target`:
210 /// - `Local(_)` → `Local(new_symbol)`
211 /// - `Global(Location)` → same location, but with the final path segment replaced
212 /// - `Unresolved` → unchanged
213 pub fn with_updated_last_symbol(self, new_symbol: Symbol) -> Self {
214 let Path { mut identifier, target, user_program, qualifier, span, id } = self;
215
216 // Update user-visible identifier
217 identifier.name = new_symbol;
218
219 let target = match target {
220 PathTarget::Unresolved => PathTarget::Unresolved,
221
222 PathTarget::Local(_) => PathTarget::Local(new_symbol),
223
224 PathTarget::Global(location) => {
225 let Location { program, mut path } = location;
226
227 assert!(!path.is_empty(), "global location must have at least one path segment");
228
229 *path.last_mut().unwrap() = new_symbol;
230
231 PathTarget::Global(Location { program, path })
232 }
233 };
234
235 Self { user_program, qualifier, identifier, target, span, id }
236 }
237
238 /// Resolves this path as a global path within the current module context.
239 ///
240 /// This function converts a user-written path into a fully qualified
241 /// [`PathTarget::Global`] by determining which program the path belongs to
242 /// and constructing the corresponding module path.
243 ///
244 /// Resolution follows two main cases:
245 ///
246 /// 1. **External library access**
247 /// If the path does not explicitly specify a program (`user_program` is `None`)
248 /// and the first qualifier segment matches a known external library name,
249 /// that segment is interpreted as the target program. The remaining qualifier
250 /// segments and identifier form the path inside that program.
251 ///
252 /// 2. **Local or explicitly-qualified program access**
253 /// Otherwise, the path is resolved relative to the current module context.
254 /// The final location is constructed by combining:
255 /// - the current module path,
256 /// - any user-written qualifier segments, and
257 /// - the final identifier.
258 ///
259 /// If the user explicitly wrote a program (via `user_program`), it overrides
260 /// the default `program` parameter. Otherwise, the current program is used.
261 ///
262 /// Importantly, this transformation **does not modify the user-written syntax**
263 /// (`user_program`, `qualifier`, `identifier`). It only determines the internal
264 /// `target` used during later compilation stages.
265 pub fn resolve_as_global_in_module<I>(
266 self,
267 program: Symbol,
268 external_libs: &IndexSet<Symbol>,
269 current_module: I,
270 ) -> Self
271 where
272 I: IntoIterator<Item = Symbol>,
273 {
274 let Path { user_program, qualifier, identifier, span, id, .. } = self;
275
276 // Case 1: The path starts with a known external library name, or with the name
277 // of the current program/library itself (a self-qualified path like `my_lib::module::item`),
278 // and the user did not explicitly specify a program. In either situation we interpret
279 // the first qualifier segment as the program name.
280 //
281 // The `first.name == program` branch handles intra-library self-qualified references
282 // (e.g., `my_lib::sub_mod::fn` written inside `my_lib`). It cannot accidentally fire
283 // for regular `.aleo` programs because program names always carry the `.aleo` suffix
284 // (e.g., `foo.aleo`), while Leo identifiers cannot contain `.`, so no qualifier can
285 // ever equal a program name.
286 if let Some(first) = qualifier.first()
287 && user_program.is_none()
288 && (external_libs.contains(&first.name) || first.name == program)
289 {
290 // Build the path within the external library by skipping the
291 // first qualifier (the library name itself).
292 let mut path: Vec<Symbol> = qualifier.iter().skip(1).map(|id| id.name).collect();
293 path.push(identifier.name);
294
295 let target = PathTarget::Global(Location { program: first.name, path });
296
297 Self { user_program: None, qualifier, identifier, target, span, id }
298 } else {
299 // Case 2: Resolve relative to the current module.
300 //
301 // Construct the path by concatenating:
302 // current_module + user qualifier + identifier.
303 let mut path: Vec<Symbol> = Vec::new();
304 path.extend(current_module);
305 path.extend(qualifier.iter().map(|id| id.name));
306 path.push(identifier.name);
307
308 // Determine which program this location belongs to:
309 // - use the explicitly written program if provided
310 // - otherwise fall back to the current program.
311 let target = PathTarget::Global(Location {
312 program: user_program.map(|id| id.as_symbol()).unwrap_or(program),
313 path,
314 });
315
316 Self { user_program, qualifier, identifier, target, span, id }
317 }
318 }
319}
320
321impl fmt::Display for Path {
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 // Determine the program prefix and separator:
324 //
325 // 1. If the user explicitly wrote a program (e.g. `credits.aleo::Foo`), always use it
326 // with a `::` separator.
327 //
328 // 2. Otherwise fall back to the resolved global location's program, but only when it is
329 // an `.aleo` program. `.aleo` programs never appear in the qualifier, so we must
330 // reconstruct the prefix here to produce readable error messages like
331 // `parent.aleo::Foo` vs `child.aleo::Foo`.
332 //
333 // 3. Library programs (no `.aleo` suffix) already have their name as the first qualifier
334 // segment (e.g. `math_lib::Foo`), so adding a prefix here would double-print it.
335 if let Some(pid) = &self.user_program {
336 write!(f, "{}::", pid.as_symbol())?;
337 } else if let Some(loc) = self.try_global_location() {
338 // Use the global program as prefix only for .aleo programs.
339 if with_session_globals(|sg| loc.program.as_str(sg, |s| s.ends_with(".aleo"))) {
340 write!(f, "{}::", loc.program)?;
341 }
342 }
343
344 // Qualifiers (always `::` separator, covers library names like `math_lib::Foo`).
345 if !self.qualifier.is_empty() {
346 write!(f, "{}::", self.qualifier.iter().map(|id| &id.name).format("::"))?;
347 }
348
349 // Final identifier
350 write!(f, "{}", self.identifier.name)
351 }
352}
353
354impl fmt::Debug for Path {
355 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356 // First, display the user-written path
357 write!(f, "{}", self)?;
358
359 // Append resolved info if available
360 match &self.target {
361 PathTarget::Local(sym) => write!(f, " [local: {sym}]"),
362 PathTarget::Global(loc) => {
363 write!(f, " [global: {loc}]")
364 }
365 PathTarget::Unresolved => write!(f, " [unresolved]"),
366 }
367 }
368}
369
370impl From<Path> for Expression {
371 fn from(value: Path) -> Self {
372 Expression::Path(value)
373 }
374}