use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::graph::unified::build::phase4e_binding::BindingEdgeIndex;
use crate::graph::unified::edge::kind::EdgeKind;
use crate::graph::unified::mutation_target::GraphMutationTarget;
use crate::graph::unified::node::id::NodeId;
use crate::graph::unified::node::kind::NodeKind;
use crate::graph::unified::string::id::StringId;
use super::scope::{ScopeArena, ScopeId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AliasEntryId(pub u32);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AliasEntry {
pub id: AliasEntryId,
pub scope: ScopeId,
pub from_symbol: StringId,
pub to_symbol: StringId,
pub import_node: NodeId,
pub is_wildcard: bool,
}
#[derive(Debug, Clone, Default)]
pub struct AliasTable {
entries: Vec<AliasEntry>,
by_scope: HashMap<ScopeId, (u32, u32)>,
}
impl AliasTable {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn entries(&self) -> &[AliasEntry] {
&self.entries
}
#[must_use]
pub fn get(&self, id: AliasEntryId) -> Option<&AliasEntry> {
self.entries.get(id.0 as usize)
}
#[must_use]
pub fn aliases_in(&self, scope: ScopeId) -> &[AliasEntry] {
match self.by_scope.get(&scope) {
Some(&(start, end)) => &self.entries[start as usize..end as usize],
None => &[],
}
}
#[must_use]
pub fn resolve_alias(&self, scope: ScopeId, symbol: StringId) -> Option<StringId> {
let slice = self.aliases_in(scope);
slice
.binary_search_by(|entry| entry.from_symbol.cmp(&symbol))
.ok()
.map(|idx| &slice[idx])
.filter(|entry| !entry.is_wildcard)
.map(|entry| entry.to_symbol)
}
#[allow(dead_code)]
pub(crate) fn retain_by_node(&mut self, keep: &dyn Fn(NodeId) -> bool) {
self.entries.retain(|entry| keep(entry.import_node));
for (idx, entry) in self.entries.iter_mut().enumerate() {
entry.id = AliasEntryId(u32::try_from(idx).expect("alias count fits u32"));
}
self.rebuild_index();
}
#[allow(dead_code)]
pub(crate) fn rewrite_string_ids_through_remap(
&mut self,
remap: &std::collections::HashMap<StringId, StringId>,
) {
if remap.is_empty() {
return;
}
let mut changed = false;
for entry in &mut self.entries {
if let Some(&canon) = remap.get(&entry.from_symbol) {
entry.from_symbol = canon;
changed = true;
}
if let Some(&canon) = remap.get(&entry.to_symbol) {
entry.to_symbol = canon;
changed = true;
}
}
if !changed {
return;
}
self.entries
.sort_by(|a, b| (a.scope, a.from_symbol).cmp(&(b.scope, b.from_symbol)));
for (idx, entry) in self.entries.iter_mut().enumerate() {
entry.id = AliasEntryId(u32::try_from(idx).expect("alias count fits u32"));
}
self.rebuild_index();
}
fn rebuild_index(&mut self) {
debug_assert!(
self.entries.windows(2).all(|w| w[0].scope <= w[1].scope),
"rebuild_index requires entries sorted by scope ascending"
);
self.by_scope.clear();
let mut cursor = 0u32;
while (cursor as usize) < self.entries.len() {
let scope = self.entries[cursor as usize].scope;
let start = cursor;
while (cursor as usize) < self.entries.len()
&& self.entries[cursor as usize].scope == scope
{
cursor += 1;
}
self.by_scope.insert(scope, (start, cursor));
}
}
}
impl Serialize for AliasTable {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.entries.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for AliasTable {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let entries = Vec::<AliasEntry>::deserialize(deserializer)?;
let mut table = AliasTable {
entries,
by_scope: HashMap::new(),
};
table.rebuild_index();
Ok(table)
}
}
pub(crate) fn derive_aliases<G: GraphMutationTarget>(
graph: &G,
scopes: &ScopeArena,
edge_index: &BindingEdgeIndex,
) -> (AliasTable, u64) {
let node_to_scope = build_node_to_scope_map(scopes);
let mut raw: Vec<AliasEntry> = Vec::new();
let mut invalid_scope_count: u64 = 0;
for &import_node_id in graph.indices().by_kind(NodeKind::Import) {
let Some(import_entry) = graph.nodes().get(import_node_id) else {
continue;
};
let Some(incoming) = edge_index.imports_by_target.get(&import_node_id) else {
continue;
};
for (source_node_id, edge_kind) in incoming {
let EdgeKind::Imports { alias, is_wildcard } = edge_kind else {
continue;
};
let scope = match node_to_scope.get(source_node_id).copied() {
Some(id) => id,
None => {
invalid_scope_count += 1;
ScopeId::INVALID
}
};
let from_symbol = alias.unwrap_or(import_entry.name);
let to_symbol = import_entry.qualified_name.unwrap_or(import_entry.name);
raw.push(AliasEntry {
id: AliasEntryId(0),
scope,
from_symbol,
to_symbol,
import_node: import_node_id,
is_wildcard: *is_wildcard,
});
}
}
raw.sort_by(|a, b| (a.scope, a.from_symbol).cmp(&(b.scope, b.from_symbol)));
for (idx, entry) in raw.iter_mut().enumerate() {
entry.id = AliasEntryId(u32::try_from(idx).expect("alias count fits u32"));
}
let mut table = AliasTable {
entries: raw,
by_scope: HashMap::new(),
};
table.rebuild_index();
(table, invalid_scope_count)
}
fn build_node_to_scope_map(scopes: &ScopeArena) -> HashMap<NodeId, ScopeId> {
scopes.iter().map(|(id, scope)| (scope.node, id)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::unified::node::id::NodeId;
use crate::graph::unified::string::id::StringId;
fn make_scope(index: u32) -> ScopeId {
ScopeId::new(index, 1)
}
fn make_node() -> NodeId {
NodeId::new(0, 1)
}
fn make_entry(scope: ScopeId, from: u32, to: u32, wildcard: bool) -> AliasEntry {
AliasEntry {
id: AliasEntryId(0), scope,
from_symbol: StringId::new(from),
to_symbol: StringId::new(to),
import_node: make_node(),
is_wildcard: wildcard,
}
}
fn table_from_sorted(mut entries: Vec<AliasEntry>) -> AliasTable {
for (idx, e) in entries.iter_mut().enumerate() {
e.id = AliasEntryId(u32::try_from(idx).unwrap());
}
let mut table = AliasTable {
entries,
by_scope: HashMap::new(),
};
table.rebuild_index();
table
}
#[test]
fn empty_table_lookup_returns_none() {
let table = AliasTable::new();
assert!(table.is_empty());
let scope = make_scope(0);
assert!(table.aliases_in(scope).is_empty());
assert_eq!(table.resolve_alias(scope, StringId::new(0)), None);
}
#[test]
fn binary_search_resolve_alias() {
let scope = make_scope(0);
let entries = vec![
make_entry(scope, 10, 100, false),
make_entry(scope, 20, 200, false),
];
let table = table_from_sorted(entries);
assert_eq!(
table.resolve_alias(scope, StringId::new(10)),
Some(StringId::new(100))
);
assert_eq!(
table.resolve_alias(scope, StringId::new(20)),
Some(StringId::new(200))
);
assert_eq!(table.resolve_alias(scope, StringId::new(30)), None);
}
#[test]
fn wildcard_skipped_by_resolve() {
let scope = make_scope(0);
let entries = vec![make_entry(scope, 5, 99, true)];
let table = table_from_sorted(entries);
assert_eq!(table.resolve_alias(scope, StringId::new(5)), None);
assert!(
table.aliases_in(scope).iter().any(|e| e.is_wildcard),
"wildcard entry visible via aliases_in"
);
}
#[test]
fn get_by_alias_entry_id_round_trips() {
let scope = make_scope(0);
let entries = vec![
make_entry(scope, 10, 100, false),
make_entry(scope, 20, 200, false),
];
let table = table_from_sorted(entries);
let e0 = table.get(AliasEntryId(0)).expect("id 0 must exist");
let e1 = table.get(AliasEntryId(1)).expect("id 1 must exist");
assert_eq!(e0.from_symbol, StringId::new(10));
assert_eq!(e0.to_symbol, StringId::new(100));
assert_eq!(e0.id, AliasEntryId(0));
assert_eq!(e1.from_symbol, StringId::new(20));
assert_eq!(e1.to_symbol, StringId::new(200));
assert_eq!(e1.id, AliasEntryId(1));
assert!(table.get(AliasEntryId(2)).is_none());
}
#[test]
fn entries_accessor_returns_full_slice() {
let scope = make_scope(0);
let entries = vec![
make_entry(scope, 1, 10, false),
make_entry(scope, 2, 20, false),
make_entry(scope, 3, 30, true),
];
let table = table_from_sorted(entries);
assert_eq!(table.entries().len(), 3);
}
#[test]
fn postcard_round_trip_preserves_entries_and_rebuilds_by_scope() {
let s_a = make_scope(1);
let s_b = make_scope(2);
let entries = vec![
make_entry(s_a, 10, 100, false),
make_entry(s_a, 11, 101, false),
make_entry(s_b, 20, 200, true),
];
let original = table_from_sorted(entries);
let bytes = postcard::to_allocvec(&original).expect("serialize");
let restored: AliasTable = postcard::from_bytes(&bytes).expect("deserialize");
assert_eq!(
restored.entries().len(),
original.entries().len(),
"entry count must survive round-trip"
);
assert_eq!(
restored.entries(),
original.entries(),
"entries must be byte-equal after round-trip"
);
assert_eq!(
restored.resolve_alias(s_a, StringId::new(10)),
Some(StringId::new(100)),
"resolve_alias on restored table must return correct target for s_a entry 0"
);
assert_eq!(
restored.resolve_alias(s_a, StringId::new(11)),
Some(StringId::new(101)),
"resolve_alias on restored table must return correct target for s_a entry 1"
);
assert_eq!(
restored.resolve_alias(s_b, StringId::new(20)),
None,
"wildcard entry must not be returned by resolve_alias after round-trip"
);
assert!(
restored.aliases_in(s_b).iter().any(|e| e.is_wildcard),
"wildcard entry in s_b must survive round-trip"
);
}
#[test]
fn aliases_in_partition_by_scope() {
let s_a = make_scope(1);
let s_b = make_scope(2);
let s_c = make_scope(3);
let from_x = StringId::new(10);
let from_y = StringId::new(11);
let to_1 = StringId::new(20);
let to_2 = StringId::new(21);
let mut entries = vec![
AliasEntry {
id: AliasEntryId(0),
scope: s_b,
from_symbol: from_x,
to_symbol: to_1,
import_node: make_node(),
is_wildcard: false,
},
AliasEntry {
id: AliasEntryId(0),
scope: s_b,
from_symbol: from_y,
to_symbol: to_2,
import_node: make_node(),
is_wildcard: false,
},
AliasEntry {
id: AliasEntryId(0),
scope: s_c,
from_symbol: from_x,
to_symbol: to_2,
import_node: make_node(),
is_wildcard: false,
},
AliasEntry {
id: AliasEntryId(0),
scope: s_c,
from_symbol: from_y,
to_symbol: to_1,
import_node: make_node(),
is_wildcard: false,
},
AliasEntry {
id: AliasEntryId(0),
scope: s_a,
from_symbol: from_x,
to_symbol: to_2,
import_node: make_node(),
is_wildcard: false,
},
AliasEntry {
id: AliasEntryId(0),
scope: s_a,
from_symbol: from_y,
to_symbol: to_1,
import_node: make_node(),
is_wildcard: false,
},
];
entries.sort_by(|a, b| (a.scope, a.from_symbol).cmp(&(b.scope, b.from_symbol)));
let table = table_from_sorted(entries);
let b_entries = table.aliases_in(s_b);
assert_eq!(b_entries.len(), 2, "s_b must have exactly 2 entries");
assert!(
b_entries.iter().all(|e| e.scope == s_b),
"s_b slice must only contain s_b entries"
);
let a_entries = table.aliases_in(s_a);
assert_eq!(a_entries.len(), 2, "s_a must have exactly 2 entries");
assert!(
a_entries.iter().all(|e| e.scope == s_a),
"s_a slice must only contain s_a entries"
);
let c_entries = table.aliases_in(s_c);
assert_eq!(c_entries.len(), 2, "s_c must have exactly 2 entries");
assert!(
c_entries.iter().all(|e| e.scope == s_c),
"s_c slice must only contain s_c entries"
);
let resolved_a = table.resolve_alias(s_a, from_x);
let resolved_b = table.resolve_alias(s_b, from_x);
assert!(resolved_a.is_some(), "s_a must resolve from_x");
assert!(resolved_b.is_some(), "s_b must resolve from_x");
assert_ne!(
resolved_a, resolved_b,
"same from_symbol in different scopes must resolve to different targets"
);
}
}