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, ScopeKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ShadowEntryId(pub u32);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShadowEntry {
pub id: ShadowEntryId,
pub scope: ScopeId,
pub symbol: StringId,
pub node: NodeId,
pub byte_offset: u32,
}
#[derive(Debug, Clone, Default)]
pub struct ShadowTable {
entries: Vec<ShadowEntry>,
chains: HashMap<(ScopeId, StringId), (u32, u32)>,
}
impl ShadowTable {
#[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) -> &[ShadowEntry] {
&self.entries
}
#[must_use]
pub fn get(&self, id: ShadowEntryId) -> Option<&ShadowEntry> {
self.entries.get(id.0 as usize)
}
#[must_use]
pub fn shadows_in(&self, scope: ScopeId) -> Vec<&ShadowEntry> {
self.entries.iter().filter(|e| e.scope == scope).collect()
}
#[must_use]
pub fn effective_binding(
&self,
scope: ScopeId,
symbol: StringId,
query_byte_offset: u32,
) -> Option<NodeId> {
let (start, end) = self.chains.get(&(scope, symbol)).copied()?;
let slice = &self.entries[start as usize..end as usize];
slice
.iter()
.rev()
.find(|e| e.byte_offset < query_byte_offset)
.map(|e| e.node)
}
#[allow(dead_code)]
pub(crate) fn retain_by_node(&mut self, keep: &dyn Fn(NodeId) -> bool) {
self.entries.retain(|entry| keep(entry.node));
for (idx, entry) in self.entries.iter_mut().enumerate() {
entry.id = ShadowEntryId(u32::try_from(idx).expect("shadow count fits u32"));
}
self.rebuild_index();
}
#[allow(dead_code)]
pub(crate) fn rewrite_string_ids_through_remap(&mut self, remap: &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.symbol) {
entry.symbol = canon;
changed = true;
}
}
if !changed {
return;
}
self.entries.sort_by(|a, b| {
(a.scope, a.symbol, a.byte_offset).cmp(&(b.scope, b.symbol, b.byte_offset))
});
for (idx, entry) in self.entries.iter_mut().enumerate() {
entry.id = ShadowEntryId(u32::try_from(idx).expect("shadow count fits u32"));
}
self.rebuild_index();
}
fn rebuild_index(&mut self) {
debug_assert!(
self.entries
.windows(2)
.all(|w| { (w[0].scope, w[0].symbol) <= (w[1].scope, w[1].symbol) }),
"rebuild_index requires entries sorted by (scope, symbol) ascending"
);
self.chains.clear();
let mut cursor = 0u32;
while (cursor as usize) < self.entries.len() {
let key = (
self.entries[cursor as usize].scope,
self.entries[cursor as usize].symbol,
);
let start = cursor;
while (cursor as usize) < self.entries.len()
&& (
self.entries[cursor as usize].scope,
self.entries[cursor as usize].symbol,
) == key
{
cursor += 1;
}
self.chains.insert(key, (start, cursor));
}
}
}
impl Serialize for ShadowTable {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.entries.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ShadowTable {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let entries = Vec::<ShadowEntry>::deserialize(deserializer)?;
let mut table = ShadowTable {
entries,
chains: HashMap::default(),
};
table.rebuild_index();
Ok(table)
}
}
pub(crate) fn derive_shadows<G: GraphMutationTarget>(
graph: &G,
scopes: &ScopeArena,
edge_index: &BindingEdgeIndex,
) -> ShadowTable {
let node_to_function_scope: HashMap<NodeId, ScopeId> = scopes
.iter()
.filter(|(_, scope)| scope.kind == ScopeKind::Function)
.map(|(id, scope)| (scope.node, id))
.collect();
let mut raw: Vec<ShadowEntry> = Vec::new();
for (&func_node, &scope_id) in &node_to_function_scope {
let Some(children) = edge_index.defines_contains_by_source.get(&func_node) else {
continue;
};
for (child, edge_kind) in children {
if !matches!(edge_kind, EdgeKind::Defines | EdgeKind::Contains) {
continue;
}
let Some(child_entry) = graph.nodes().get(*child) else {
continue;
};
if !matches!(child_entry.kind, NodeKind::Variable | NodeKind::Parameter) {
continue;
}
raw.push(ShadowEntry {
id: ShadowEntryId(0),
scope: scope_id,
symbol: child_entry.name,
node: *child,
byte_offset: child_entry.start_byte,
});
}
}
if raw.is_empty() {
log::debug!("derive_shadows: no Variable/Parameter nodes under Function scopes");
return ShadowTable::new();
}
raw.sort_by(|a, b| (a.scope, a.symbol, a.byte_offset).cmp(&(b.scope, b.symbol, b.byte_offset)));
for (idx, entry) in raw.iter_mut().enumerate() {
entry.id = ShadowEntryId(u32::try_from(idx).expect("shadow count fits u32"));
}
let mut table = ShadowTable {
entries: raw,
chains: HashMap::default(),
};
table.rebuild_index();
log::debug!(
"derive_shadows: derived {} shadow entries across {} chains",
table.len(),
table.chains.len(),
);
table
}
#[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 table_from_sorted(mut entries: Vec<ShadowEntry>) -> ShadowTable {
for (idx, e) in entries.iter_mut().enumerate() {
e.id = ShadowEntryId(u32::try_from(idx).unwrap());
}
let mut table = ShadowTable {
entries,
chains: HashMap::default(),
};
table.rebuild_index();
table
}
fn make_entry(scope: ScopeId, symbol: StringId, node: NodeId, byte_offset: u32) -> ShadowEntry {
ShadowEntry {
id: ShadowEntryId(0), scope,
symbol,
node,
byte_offset,
}
}
#[test]
fn t12_effective_binding_innermost_before_offset() {
let scope = make_scope(0);
let symbol = StringId::new(1);
let entries = vec![
make_entry(scope, symbol, NodeId::new(1, 1), 10),
make_entry(scope, symbol, NodeId::new(2, 1), 30),
];
let table = table_from_sorted(entries);
assert_eq!(
table.effective_binding(scope, symbol, 40),
Some(NodeId::new(2, 1)),
"innermost definition before offset 40 must be node 2 (at offset 30)"
);
assert_eq!(
table.effective_binding(scope, symbol, 20),
Some(NodeId::new(1, 1)),
"only node 1 (offset 10) is before offset 20"
);
}
#[test]
fn t13_effective_binding_before_first_returns_none() {
let scope = make_scope(0);
let symbol = StringId::new(1);
let entries = vec![make_entry(scope, symbol, NodeId::new(1, 1), 50)];
let table = table_from_sorted(entries);
assert_eq!(
table.effective_binding(scope, symbol, 10),
None,
"query before first definition must return None"
);
}
#[test]
fn empty_table_behavior() {
let table = ShadowTable::new();
assert!(table.is_empty());
assert_eq!(table.len(), 0);
let scope = make_scope(0);
let symbol = StringId::new(1);
assert!(table.shadows_in(scope).is_empty());
assert_eq!(table.effective_binding(scope, symbol, 100), None);
assert!(table.get(ShadowEntryId(0)).is_none());
}
#[test]
fn get_by_shadow_entry_id_round_trips() {
let scope = make_scope(0);
let sym_a = StringId::new(1);
let sym_b = StringId::new(2);
let entries = vec![
make_entry(scope, sym_a, NodeId::new(1, 1), 10),
make_entry(scope, sym_b, NodeId::new(2, 1), 20),
];
let table = table_from_sorted(entries);
let e0 = table.get(ShadowEntryId(0)).expect("id 0 must exist");
let e1 = table.get(ShadowEntryId(1)).expect("id 1 must exist");
assert_eq!(e0.id, ShadowEntryId(0));
assert_eq!(e0.symbol, sym_a);
assert_eq!(e1.id, ShadowEntryId(1));
assert_eq!(e1.symbol, sym_b);
assert!(table.get(ShadowEntryId(2)).is_none());
}
#[test]
fn entries_accessor_returns_full_slice() {
let scope = make_scope(0);
let symbol = StringId::new(1);
let entries = vec![
make_entry(scope, symbol, NodeId::new(1, 1), 5),
make_entry(scope, symbol, NodeId::new(2, 1), 15),
make_entry(scope, symbol, NodeId::new(3, 1), 25),
];
let table = table_from_sorted(entries);
assert_eq!(table.entries().len(), 3);
}
#[test]
fn scope_partitioning_no_collision() {
let s_a = make_scope(0);
let s_b = make_scope(1);
let sym = StringId::new(42);
let entries = vec![
make_entry(s_a, sym, NodeId::new(10, 1), 5),
make_entry(s_a, sym, NodeId::new(11, 1), 20),
make_entry(s_b, sym, NodeId::new(20, 1), 5),
make_entry(s_b, sym, NodeId::new(21, 1), 30),
];
let table = table_from_sorted(entries);
assert_eq!(
table.effective_binding(s_a, sym, 25),
Some(NodeId::new(11, 1)),
"s_a effective binding at 25 must be node 11"
);
assert_eq!(
table.effective_binding(s_b, sym, 25),
Some(NodeId::new(20, 1)),
"s_b effective binding at 25 must be node 20"
);
assert_ne!(
table.effective_binding(s_a, sym, 25),
table.effective_binding(s_b, sym, 25),
"same symbol in different scopes must resolve independently"
);
}
#[test]
fn postcard_round_trip_preserves_entries_and_rebuilds_chains() {
let s_a = make_scope(1);
let s_b = make_scope(2);
let sym_x = StringId::new(10);
let sym_y = StringId::new(11);
let entries = vec![
make_entry(s_a, sym_x, NodeId::new(1, 1), 10),
make_entry(s_a, sym_x, NodeId::new(2, 1), 30),
make_entry(s_b, sym_y, NodeId::new(3, 1), 5),
];
let original = table_from_sorted(entries);
let bytes = postcard::to_allocvec(&original).expect("serialize");
let restored: ShadowTable = 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.effective_binding(s_a, sym_x, 40),
Some(NodeId::new(2, 1)),
"effective_binding on restored table must find node 2 for s_a/sym_x at 40"
);
assert_eq!(
restored.effective_binding(s_a, sym_x, 20),
Some(NodeId::new(1, 1)),
"effective_binding on restored table must find node 1 for s_a/sym_x at 20"
);
assert_eq!(
restored.effective_binding(s_b, sym_y, 10),
Some(NodeId::new(3, 1)),
"effective_binding on restored table must find node 3 for s_b/sym_y at 10"
);
assert_eq!(
restored.effective_binding(s_b, sym_y, 3),
None,
"query before first def must return None after round-trip"
);
}
}