use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::graph::unified::file::id::FileId;
use crate::graph::unified::node::id::NodeId;
use crate::graph::unified::string::StringId;
pub mod scope_index;
pub use scope_index::{LocalDeclaration, LocalScopeIndex, ScopeEntry};
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CIndirectSideTables {
pub fn_signature: HashMap<NodeId, StringId>,
pub struct_field_fnptr: HashMap<(StringId, StringId), StringId>,
pub local_var_type: HashMap<NodeId, StringId>,
pub local_scope_indices: HashMap<FileId, LocalScopeIndex>,
pub bindings_by_field: HashMap<(StringId, StringId), Vec<BindingEntry>>,
pub pending_callsites: Vec<IndirectCallsite>,
}
impl CIndirectSideTables {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.fn_signature.is_empty()
&& self.struct_field_fnptr.is_empty()
&& self.local_var_type.is_empty()
&& self.local_scope_indices.is_empty()
&& self.bindings_by_field.is_empty()
&& self.pending_callsites.is_empty()
}
#[inline]
#[must_use]
pub fn scope_index_for(&self, file_id: FileId) -> Option<&LocalScopeIndex> {
self.local_scope_indices.get(&file_id)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BindingEntry {
pub instance_node: NodeId,
pub target_fn: NodeId,
pub site_kind: BindingSiteKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BindingSiteKind {
DesignatedInitializer,
PositionalInitializer,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IndirectCallsite {
pub caller: NodeId,
pub file_id: FileId,
pub use_span: (usize, usize),
pub shape: IndirectShape,
pub argument_count: u32,
pub is_async: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum IndirectShape {
PointerExpr {
var_name: String,
},
FieldExpr {
receiver_name: String,
field_name: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::unified::file::id::FileId;
use crate::graph::unified::node::id::NodeId;
use crate::graph::unified::string::StringId;
#[test]
fn option_none_serializes_to_one_byte() {
let none: Option<CIndirectSideTables> = None;
let bytes = postcard::to_stdvec(&none).expect("serialize None");
assert_eq!(
bytes.len(),
1,
"Option<CIndirectSideTables>::None must encode as a single \
discriminant byte (got {} bytes: {:?})",
bytes.len(),
bytes
);
assert_eq!(
bytes[0], 0x00,
"postcard encodes Option::None as the 0x00 discriminant"
);
let decoded: Option<CIndirectSideTables> =
postcard::from_bytes(&bytes).expect("deserialize None");
assert_eq!(decoded, None);
}
#[test]
fn populated_table_roundtrip_preserves_every_map() {
let node_a = NodeId::new(1, 1);
let node_b = NodeId::new(2, 1);
let node_caller = NodeId::new(3, 1);
let node_instance = NodeId::new(4, 1);
let node_target_fn = NodeId::new(5, 1);
let node_local_var = NodeId::new(6, 1);
let sig_a = StringId::new(101);
let struct_qn = StringId::new(200);
let field_name = StringId::new(201);
let field_sig = StringId::new(202);
let local_var_type_tok = StringId::new(300);
let file_id_a = FileId::new(7);
let scope_index = LocalScopeIndex::from_parts(
vec![
ScopeEntry::new((0, 100), None), ScopeEntry::new((20, 60), Some(0)), ],
vec![
vec![
LocalDeclaration::new("x".into(), "int".into(), (5, 12), 0),
LocalDeclaration::new("fp".into(), "void (*)(int)".into(), (13, 30), 0),
],
vec![LocalDeclaration::new(
"y".into(),
"char".into(),
(25, 35),
1,
)],
],
);
assert_eq!(scope_index.resolve_type("x", 40), Some("int"));
assert_eq!(scope_index.resolve_type("y", 40), Some("char"));
assert_eq!(scope_index.resolve_type("fp", 40), Some("void (*)(int)"));
assert_eq!(scope_index.resolve_type("x", 1), None);
let mut tables = CIndirectSideTables::new();
tables.fn_signature.insert(node_a, sig_a);
tables
.struct_field_fnptr
.insert((struct_qn, field_name), field_sig);
tables
.local_var_type
.insert(node_local_var, local_var_type_tok);
tables
.local_scope_indices
.insert(file_id_a, scope_index.clone());
tables.bindings_by_field.insert(
(struct_qn, field_name),
vec![
BindingEntry {
instance_node: node_instance,
target_fn: node_target_fn,
site_kind: BindingSiteKind::DesignatedInitializer,
},
BindingEntry {
instance_node: node_b,
target_fn: node_a,
site_kind: BindingSiteKind::PositionalInitializer,
},
],
);
tables.pending_callsites.push(IndirectCallsite {
caller: node_caller,
file_id: file_id_a,
use_span: (40, 48),
shape: IndirectShape::PointerExpr {
var_name: "fp".into(),
},
argument_count: 2,
is_async: false,
});
tables.pending_callsites.push(IndirectCallsite {
caller: node_caller,
file_id: file_id_a,
use_span: (50, 70),
shape: IndirectShape::FieldExpr {
receiver_name: "ops".into(),
field_name: "read".into(),
},
argument_count: 3,
is_async: false,
});
assert!(
!tables.is_empty(),
"constructed tables must report non-empty"
);
let some_tables = Some(tables.clone());
let bytes = postcard::to_stdvec(&some_tables).expect("serialize Some");
assert!(
bytes.len() > 1,
"populated Some(...) must encode more than the bare 0x01 \
discriminant; got {} bytes",
bytes.len()
);
assert_eq!(
bytes[0], 0x01,
"postcard encodes Option::Some as the 0x01 discriminant"
);
let decoded: Option<CIndirectSideTables> =
postcard::from_bytes(&bytes).expect("deserialize Some");
assert_eq!(
decoded,
Some(tables.clone()),
"populated CIndirectSideTables must deep-roundtrip via postcard"
);
let decoded_tables = decoded.expect("Some after roundtrip");
assert_eq!(decoded_tables.fn_signature.get(&node_a), Some(&sig_a));
assert_eq!(
decoded_tables
.struct_field_fnptr
.get(&(struct_qn, field_name)),
Some(&field_sig)
);
assert_eq!(
decoded_tables.local_var_type.get(&node_local_var),
Some(&local_var_type_tok)
);
let restored_scope = decoded_tables
.local_scope_indices
.get(&file_id_a)
.expect("scope index restored under file_id_a");
assert_eq!(restored_scope.resolve_type("x", 40), Some("int"));
assert_eq!(restored_scope.resolve_type("y", 40), Some("char"));
let bindings = decoded_tables
.bindings_by_field
.get(&(struct_qn, field_name))
.expect("binding entries restored under (struct_qn, field_name)");
assert_eq!(bindings.len(), 2);
assert_eq!(
bindings[0].site_kind,
BindingSiteKind::DesignatedInitializer
);
assert_eq!(
bindings[1].site_kind,
BindingSiteKind::PositionalInitializer
);
assert_eq!(decoded_tables.pending_callsites.len(), 2);
assert!(matches!(
&decoded_tables.pending_callsites[0].shape,
IndirectShape::PointerExpr { var_name } if var_name == "fp"
));
assert!(matches!(
&decoded_tables.pending_callsites[1].shape,
IndirectShape::FieldExpr { receiver_name, field_name }
if receiver_name == "ops" && field_name == "read"
));
assert!(decoded_tables.scope_index_for(file_id_a).is_some());
assert!(decoded_tables.scope_index_for(FileId::new(9999)).is_none());
}
#[test]
fn new_tables_are_empty() {
let tables = CIndirectSideTables::new();
assert!(tables.is_empty());
assert!(tables.fn_signature.is_empty());
assert!(tables.struct_field_fnptr.is_empty());
assert!(tables.local_var_type.is_empty());
assert!(tables.local_scope_indices.is_empty());
assert!(tables.bindings_by_field.is_empty());
assert!(tables.pending_callsites.is_empty());
}
}