1use alloc::{collections::BTreeMap, sync::Arc, vec::Vec};
2
3use miden_debug_types::{SourceManager, SourceSpan, Span, Spanned};
4
5use super::{SymbolResolution, SymbolResolutionError};
6use crate::{
7 Path,
8 ast::{AliasTarget, Ident, ItemIndex},
9};
10
11const MAX_ALIAS_EXPANSION_DEPTH: usize = 128;
16
17pub trait SymbolTable {
22 type SymbolIter: Iterator<Item = LocalSymbol>;
24
25 fn symbols(&self, source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter;
28
29 fn checked_symbols(
35 &self,
36 source_manager: Arc<dyn SourceManager>,
37 ) -> Result<Self::SymbolIter, SymbolResolutionError> {
38 Ok(self.symbols(source_manager))
39 }
40}
41
42impl SymbolTable for &crate::library::ModuleInfo {
43 type SymbolIter = alloc::vec::IntoIter<LocalSymbol>;
44
45 fn symbols(&self, _source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter {
46 let mut items = Vec::with_capacity(self.raw_items().len());
47
48 for (i, item) in self.raw_items().iter().enumerate() {
49 let name = item.name().clone();
50 let span = name.span();
51 items.push(LocalSymbol::Item {
52 name,
53 resolved: SymbolResolution::Local(Span::new(span, ItemIndex::new(i))),
54 });
55 }
56
57 items.into_iter()
58 }
59
60 fn checked_symbols(
61 &self,
62 source_manager: Arc<dyn SourceManager>,
63 ) -> Result<Self::SymbolIter, SymbolResolutionError> {
64 if self.raw_items().len() > ItemIndex::MAX_ITEMS {
65 Err(SymbolResolutionError::too_many_items_in_module(
66 SourceSpan::UNKNOWN,
67 &*source_manager,
68 ))
69 } else {
70 Ok(self.symbols(source_manager))
71 }
72 }
73}
74
75impl SymbolTable for &crate::ast::Module {
76 type SymbolIter = alloc::vec::IntoIter<LocalSymbol>;
77
78 fn symbols(&self, source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter {
79 use crate::ast::{AliasTarget, Export};
80
81 let mut items = Vec::with_capacity(self.items.len());
82
83 for (i, item) in self.items.iter().enumerate() {
84 let id = ItemIndex::new(i);
85 let name = item.name().clone();
86 let span = name.span();
87 let name = name.into_inner();
88
89 if let Export::Alias(alias) = item {
90 match alias.target() {
91 AliasTarget::MastRoot(root) => {
92 items.push(LocalSymbol::Import {
93 name: Span::new(span, name),
94 resolution: Ok(SymbolResolution::MastRoot(*root)),
95 });
96 },
97 AliasTarget::Path(path) => {
98 let expanded = LocalSymbolTable::expand(
99 |name| self.get_import(name).map(|alias| alias.target().clone()),
100 path.as_deref(),
101 &source_manager,
102 );
103 items.push(LocalSymbol::Import {
104 name: Span::new(span, name),
105 resolution: expanded,
106 });
107 },
108 }
109 } else {
110 items.push(LocalSymbol::Item {
111 name: Ident::from_raw_parts(Span::new(span, name)),
112 resolved: SymbolResolution::Local(Span::new(span, id)),
113 });
114 }
115 }
116
117 items.into_iter()
118 }
119
120 fn checked_symbols(
121 &self,
122 source_manager: Arc<dyn SourceManager>,
123 ) -> Result<Self::SymbolIter, SymbolResolutionError> {
124 if self.items.len() > ItemIndex::MAX_ITEMS {
125 Err(SymbolResolutionError::too_many_items_in_module(self.span(), &*source_manager))
126 } else {
127 Ok(self.symbols(source_manager))
128 }
129 }
130}
131
132#[derive(Debug)]
134pub enum LocalSymbol {
135 Item { name: Ident, resolved: SymbolResolution },
137 Import {
139 name: Span<Arc<str>>,
140 resolution: Result<SymbolResolution, SymbolResolutionError>,
141 },
142}
143
144impl LocalSymbol {
145 pub fn name(&self) -> &str {
146 match self {
147 Self::Item { name, .. } => name.as_str(),
148 Self::Import { name, .. } => name,
149 }
150 }
151}
152
153pub(super) struct LocalSymbolTable {
155 source_manager: Arc<dyn SourceManager>,
156 symbols: BTreeMap<Arc<str>, ItemIndex>,
157 items: Vec<LocalSymbol>,
158}
159
160impl core::ops::Index<ItemIndex> for LocalSymbolTable {
161 type Output = LocalSymbol;
162
163 #[inline(always)]
164 fn index(&self, index: ItemIndex) -> &Self::Output {
165 &self.items[index.as_usize()]
166 }
167}
168
169impl LocalSymbolTable {
170 fn build<I>(iter: I, source_manager: Arc<dyn SourceManager>) -> Self
171 where
172 I: Iterator<Item = LocalSymbol>,
173 {
174 let mut symbols = BTreeMap::default();
175 let mut items = Vec::with_capacity(16);
176
177 for (i, symbol) in iter.enumerate() {
178 let id = ItemIndex::try_new(i)
179 .expect("symbol iterators used by LocalSymbolTable::build must be pre-validated");
180 let symbol = match symbol {
181 LocalSymbol::Item {
182 name,
183 resolved: SymbolResolution::Local(local),
184 } => LocalSymbol::Item {
185 name,
186 resolved: SymbolResolution::Local(Span::new(local.span(), id)),
187 },
188 symbol => symbol,
189 };
190 log::debug!(target: "symbol-table::new", "registering {} symbol: {}", match symbol {
191 LocalSymbol::Item { .. } => "local",
192 LocalSymbol::Import { .. } => "imported",
193 }, symbol.name());
194 let name = match &symbol {
195 LocalSymbol::Item { name, .. } => name.clone().into_inner(),
196 LocalSymbol::Import { name, .. } => name.clone().into_inner(),
197 };
198
199 if let Some(prev) = symbols.get(&name).copied() {
200 debug_assert!(
201 false,
202 "duplicate symbol '{name}' reached local resolver construction (previous={prev:?}, current={id:?})"
203 );
204 } else {
205 symbols.insert(name.clone(), id);
206 }
207 items.push(symbol);
208 }
209
210 Self { source_manager, symbols, items }
211 }
212
213 pub fn new<S>(
214 iter: S,
215 source_manager: Arc<dyn SourceManager>,
216 ) -> Result<Self, SymbolResolutionError>
217 where
218 S: SymbolTable,
219 {
220 let symbols = iter.checked_symbols(source_manager.clone())?;
221 Ok(Self::build(symbols, source_manager))
222 }
223}
224
225impl LocalSymbolTable {
226 pub fn get(&self, name: Span<&str>) -> Result<SymbolResolution, SymbolResolutionError> {
236 log::debug!(target: "symbol-table", "attempting to resolve '{name}'");
237 let (span, name) = name.into_parts();
238 let Some(item) = self.symbols.get(name).copied() else {
239 return Err(SymbolResolutionError::undefined(span, &self.source_manager));
240 };
241 match &self.items[item.as_usize()] {
242 LocalSymbol::Item { resolved, .. } => {
243 log::debug!(target: "symbol-table", "resolved '{name}' to {resolved:?}");
244 Ok(resolved.clone())
245 },
246 LocalSymbol::Import { name, resolution } => {
247 log::debug!(target: "symbol-table", "'{name}' refers to an import");
248 match resolution {
249 Ok(resolved) => {
250 log::debug!(target: "symbol-table", "resolved '{name}' to {resolved:?}");
251 Ok(resolved.clone())
252 },
253 Err(err) => {
254 log::error!(target: "symbol-table", "resolution of '{name}' failed: {err}");
255 Err(err.clone())
256 },
257 }
258 },
259 }
260 }
261
262 pub fn expand<F>(
296 get_import: F,
297 path: Span<&Path>,
298 source_manager: &dyn SourceManager,
299 ) -> Result<SymbolResolution, SymbolResolutionError>
300 where
301 F: Fn(&str) -> Option<AliasTarget>,
302 {
303 let mut expansion_stack = Vec::new();
304 Self::expand_with_guard(get_import, path, source_manager, &mut expansion_stack)
305 }
306
307 fn expand_with_guard<F>(
308 get_import: F,
309 path: Span<&Path>,
310 source_manager: &dyn SourceManager,
311 expansion_stack: &mut Vec<Arc<Path>>,
312 ) -> Result<SymbolResolution, SymbolResolutionError>
313 where
314 F: Fn(&str) -> Option<AliasTarget>,
315 {
316 if expansion_stack.len() > MAX_ALIAS_EXPANSION_DEPTH {
317 return Err(SymbolResolutionError::alias_expansion_depth_exceeded(
318 path.span(),
319 MAX_ALIAS_EXPANSION_DEPTH,
320 source_manager,
321 ));
322 }
323
324 let path_ref: &Path = *path;
325 if expansion_stack.iter().any(|entry| entry.as_ref() == path_ref) {
326 return Err(SymbolResolutionError::alias_expansion_cycle(path.span(), source_manager));
327 }
328
329 expansion_stack.push(Arc::from(path_ref));
330
331 let result = {
332 let (module_name, rest) = path.split_first().unwrap();
333 if let Some(target) = get_import(module_name) {
334 match target {
335 AliasTarget::MastRoot(digest) if rest.is_empty() => {
336 Ok(SymbolResolution::MastRoot(digest))
337 },
338 AliasTarget::MastRoot(digest) => {
339 Err(SymbolResolutionError::invalid_alias_target(
340 digest.span(),
341 path.span(),
342 source_manager,
343 ))
344 },
345 AliasTarget::Path(shadowed) if shadowed.as_deref() == path => {
354 Ok(SymbolResolution::External(shadowed))
355 },
356 AliasTarget::Path(path) => {
357 let resolved = Self::expand_with_guard(
358 get_import,
359 path.as_deref(),
360 source_manager,
361 expansion_stack,
362 )?;
363 match resolved {
364 SymbolResolution::Module { id, path } => {
365 if rest.is_empty() {
368 Ok(SymbolResolution::Module { id, path })
369 } else {
370 Ok(SymbolResolution::External(
371 path.map(|p| p.join(rest).into()),
372 ))
373 }
374 },
375 SymbolResolution::External(resolved) => {
376 Ok(SymbolResolution::External(
379 resolved.map(|p| p.to_absolute().join(rest).into()),
380 ))
381 },
382 res @ (SymbolResolution::MastRoot(_)
383 | SymbolResolution::Local(_)
384 | SymbolResolution::Exact { .. })
385 if rest.is_empty() =>
386 {
387 Ok(res)
388 },
389 SymbolResolution::MastRoot(digest) => {
390 Err(SymbolResolutionError::invalid_alias_target(
391 digest.span(),
392 path.span(),
393 source_manager,
394 ))
395 },
396 SymbolResolution::Exact { path: item_path, .. } => {
397 Err(SymbolResolutionError::invalid_alias_target(
398 item_path.span(),
399 path.span(),
400 source_manager,
401 ))
402 },
403 SymbolResolution::Local(item) => {
404 Err(SymbolResolutionError::invalid_alias_target(
405 item.span(),
406 path.span(),
407 source_manager,
408 ))
409 },
410 }
411 },
412 }
413 } else {
414 Ok(SymbolResolution::External(path.map(|p| p.to_absolute().into_owned().into())))
417 }
418 };
419
420 expansion_stack.pop();
421 result
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use alloc::{
428 collections::BTreeMap,
429 string::{String, ToString},
430 sync::Arc,
431 };
432 use core::str::FromStr;
433
434 use miden_debug_types::DefaultSourceManager;
435
436 use super::*;
437 use crate::PathBuf;
438
439 fn path_arc(path: &str) -> Arc<Path> {
440 let path = PathBuf::from_str(path).expect("valid path");
441 Arc::from(path.as_path())
442 }
443
444 #[test]
445 fn alias_expansion_detects_cycle() {
446 let source_manager = DefaultSourceManager::default();
447 let mut imports = BTreeMap::<String, AliasTarget>::new();
448 imports.insert("a".to_string(), AliasTarget::Path(Span::unknown(path_arc("b"))));
449 imports.insert("b".to_string(), AliasTarget::Path(Span::unknown(path_arc("a"))));
450
451 let path = PathBuf::from_str("a").expect("valid path");
452 let result = LocalSymbolTable::expand(
453 |name| imports.get(name).cloned(),
454 Span::unknown(path.as_path()),
455 &source_manager,
456 );
457
458 assert!(matches!(result, Err(SymbolResolutionError::AliasExpansionCycle { .. })));
459 }
460
461 #[test]
462 fn alias_expansion_depth_boundary() {
463 let source_manager = DefaultSourceManager::default();
464 let mut imports = BTreeMap::<String, AliasTarget>::new();
465 let max_depth = MAX_ALIAS_EXPANSION_DEPTH;
466 for i in 0..max_depth {
467 let current = format!("a{i}");
468 let next = format!("a{}", i + 1);
469 imports.insert(current, AliasTarget::Path(Span::unknown(path_arc(&next))));
470 }
471
472 let path = PathBuf::from_str("a0").expect("valid path");
473 let result = LocalSymbolTable::expand(
474 |name| imports.get(name).cloned(),
475 Span::unknown(path.as_path()),
476 &source_manager,
477 )
478 .expect("expected depth boundary to resolve");
479
480 match result {
481 SymbolResolution::External(resolved) => {
482 let expected = format!("a{max_depth}");
483 let expected = PathBuf::from_str(&expected).expect("valid path");
484 let expected = expected.as_path().to_absolute().into_owned();
485 assert_eq!(resolved.as_deref(), expected.as_path());
486 },
487 other => panic!("expected external resolution, got {other:?}"),
488 }
489 }
490
491 #[test]
492 fn alias_expansion_depth_exceeded() {
493 let source_manager = DefaultSourceManager::default();
494 let mut imports = BTreeMap::<String, AliasTarget>::new();
495 for i in 0..=MAX_ALIAS_EXPANSION_DEPTH {
496 let current = format!("a{i}");
497 let next = format!("a{}", i + 1);
498 imports.insert(current, AliasTarget::Path(Span::unknown(path_arc(&next))));
499 }
500
501 let path = PathBuf::from_str("a0").expect("valid path");
502 let result = LocalSymbolTable::expand(
503 |name| imports.get(name).cloned(),
504 Span::unknown(path.as_path()),
505 &source_manager,
506 );
507
508 assert!(matches!(
509 result,
510 Err(SymbolResolutionError::AliasExpansionDepthExceeded { max_depth, .. })
511 if max_depth == MAX_ALIAS_EXPANSION_DEPTH
512 ));
513 }
514
515 #[test]
516 fn alias_expansion_handles_shadowed_import() {
517 let source_manager = DefaultSourceManager::default();
518 let mut imports = BTreeMap::<String, AliasTarget>::new();
519 imports.insert("lib".to_string(), AliasTarget::Path(Span::unknown(path_arc("lib"))));
520
521 let path = PathBuf::from_str("lib").expect("valid path");
522 let result = LocalSymbolTable::expand(
523 |name| imports.get(name).cloned(),
524 Span::unknown(path.as_path()),
525 &source_manager,
526 )
527 .expect("shadowed import should resolve");
528
529 match result {
530 SymbolResolution::External(resolved) => {
531 assert_eq!(resolved.as_deref(), path.as_path());
532 },
533 other => panic!("expected external resolution, got {other:?}"),
534 }
535 }
536
537 #[test]
538 fn checked_symbols_rejects_oversized_module() {
539 let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
540 let mut module =
541 crate::ast::Module::new(crate::ast::ModuleKind::Library, Path::new("::m::huge"));
542
543 for i in 0..=ItemIndex::MAX_ITEMS {
544 module.items.push(crate::ast::Export::Constant(crate::ast::Constant::new(
545 SourceSpan::UNKNOWN,
546 crate::ast::Visibility::Private,
547 Ident::new(format!("A{i}")).expect("valid identifier"),
548 crate::ast::ConstantExpr::Int(Span::unknown(crate::parser::IntValue::from(0u8))),
549 )));
550 }
551
552 let result = (&module).checked_symbols(source_manager);
553
554 assert!(matches!(result, Err(SymbolResolutionError::TooManyItemsInModule { .. })));
555 }
556
557 #[test]
558 fn checked_symbols_guard_custom_symbol_table_exact() {
559 struct ExactTooManySymbols;
560
561 impl SymbolTable for ExactTooManySymbols {
562 type SymbolIter = alloc::vec::IntoIter<LocalSymbol>;
563
564 fn symbols(&self, _source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter {
565 panic!("exact construction must not request unchecked symbols")
566 }
567
568 fn checked_symbols(
569 &self,
570 source_manager: Arc<dyn SourceManager>,
571 ) -> Result<Self::SymbolIter, SymbolResolutionError> {
572 Err(SymbolResolutionError::too_many_items_in_module(
573 SourceSpan::UNKNOWN,
574 &*source_manager,
575 ))
576 }
577 }
578
579 let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
580 let result = LocalSymbolTable::new(ExactTooManySymbols, source_manager);
581
582 assert!(matches!(result, Err(SymbolResolutionError::TooManyItemsInModule { .. })));
583 }
584
585 #[cfg(test)]
586 struct DuplicateSymbolsForInvariantTest;
587
588 #[cfg(test)]
589 impl SymbolTable for DuplicateSymbolsForInvariantTest {
590 type SymbolIter = alloc::vec::IntoIter<LocalSymbol>;
591
592 fn symbols(&self, _source_manager: Arc<dyn SourceManager>) -> Self::SymbolIter {
593 let first = LocalSymbol::Item {
594 name: Ident::new("dup").expect("valid identifier"),
595 resolved: SymbolResolution::Local(Span::unknown(ItemIndex::new(0))),
596 };
597 let second = LocalSymbol::Item {
598 name: Ident::new("dup").expect("valid identifier"),
599 resolved: SymbolResolution::Local(Span::unknown(ItemIndex::new(1))),
600 };
601 alloc::vec![first, second].into_iter()
602 }
603 }
604
605 #[cfg(debug_assertions)]
606 #[test]
607 #[should_panic(expected = "duplicate symbol 'dup' reached local resolver construction")]
608 fn local_symbol_table_rejects_duplicate_symbols() {
609 let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
610 let _table = LocalSymbolTable::new(DuplicateSymbolsForInvariantTest, source_manager);
611 }
612
613 #[test]
614 fn local_symbol_table_duplicate_symbols_have_explicit_behavior() {
615 use std::panic::{AssertUnwindSafe, catch_unwind};
616
617 let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
618 let result = catch_unwind(AssertUnwindSafe(|| {
619 LocalSymbolTable::new(DuplicateSymbolsForInvariantTest, source_manager)
620 }));
621
622 if cfg!(debug_assertions) {
623 assert!(
624 result.is_err(),
625 "debug builds should panic when duplicates reach local resolver construction"
626 );
627 } else {
628 let table = result
629 .expect("release builds should not panic on duplicate symbols")
630 .expect("release builds should still construct a table");
631 let resolved = table
632 .get(Span::unknown("dup"))
633 .expect("release behavior should keep a deterministic symbol mapping");
634 match resolved {
635 SymbolResolution::Local(id) => assert_eq!(id.into_inner(), ItemIndex::new(0)),
636 other => panic!("expected local symbol resolution, got {other:?}"),
637 }
638 }
639 }
640}