Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ AST to save in the graph
- `rust/rubydex/src/resolution.rs`: the Resolution stage that computes fully qualified names, creates declarations,
resolves constant references, and linearizes ancestor chains

Inheritance edges: namespaces store only *immediate* descendants (direct superclass/include/prepend children, plus
`extend`-via-singleton). For transitive descendants, call `Graph::transitive_descendants(root)` — a BFS iterator over
the direct-child DAG with dedup. Never add a stored transitive set; it would defeat the memory-bounded invariant.

### Commands

When necessary, commands can be executed for the Rust code.
Expand Down
8 changes: 8 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ Rubydex represents the codebase as a graph, where entities are nodes and relatio
- `model/declaration.rs`: Global declarations produced during resolution
- `model/graph.rs`: The main graph structure containing all entities

### Inheritance edges

Each namespace stores only its **immediate** descendants — the declarations that directly inherit from, include, or prepend it. For singleton classes, the edge is recorded against the singleton from an `extend`.

Transitive descendants are computed on demand via `Graph::transitive_descendants(root)`, a BFS iterator over the direct-child DAG with visited-set dedup.

Use the iterator (not the stored field) anywhere you need the full transitive set — the stored field is intentionally the direct-edge subset to keep memory bounded.

### ID Types

Connections between nodes use hashed IDs defined in `ids.rs`:
Expand Down
12 changes: 6 additions & 6 deletions rust/rubydex-mcp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,19 +379,19 @@ impl RubydexServer {
fn get_descendants(&self, Parameters(params): Parameters<GetDescendantsParams>) -> String {
let state = ensure_graph_ready!(self);
let graph = state.graph.as_ref().unwrap();
let (_, decl) = lookup_declaration!(graph, &params.name);
let namespace = require_namespace!(decl, &params.name, "get_descendants");
let (decl_id, decl) = lookup_declaration!(graph, &params.name);
require_namespace!(decl, &params.name, "get_descendants");

let limit = params.limit.filter(|&l| l > 0).unwrap_or(50).min(500); // default 50, max 500
let offset = params.offset.unwrap_or(0);

let (descendants, total) = paginate!(
namespace.descendants().iter(),
graph.transitive_descendants(decl_id),
offset,
limit,
|id| graph.declarations().get(id).is_some(),
|id| {
let desc_decl = graph.declarations().get(id)?;
|id: &DeclarationId| graph.declarations().get(id).is_some(),
|id: DeclarationId| {
let desc_decl = graph.declarations().get(&id)?;
Some(serde_json::json!({
"name": desc_decl.name(),
"kind": desc_decl.kind(),
Expand Down
14 changes: 8 additions & 6 deletions rust/rubydex-sys/src/declaration_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,14 +382,16 @@ pub unsafe extern "C" fn rdx_declaration_descendants(pointer: GraphPointer, decl
let declarations = with_graph(pointer, |graph| {
let declaration_id = DeclarationId::new(decl_id);

let Some(Declaration::Namespace(declaration)) = graph.declarations().get(&declaration_id) else {
if !matches!(
graph.declarations().get(&declaration_id),
Some(Declaration::Namespace(_))
) {
return Vec::new();
};
}

declaration
.descendants()
.iter()
.map(|id| CDeclaration::from_declaration(*id, graph.declarations().get(id).unwrap()))
graph
.transitive_descendants(declaration_id)
.map(|id| CDeclaration::from_declaration(id, graph.declarations().get(&id).unwrap()))
.collect::<Vec<_>>()
});

Expand Down
43 changes: 18 additions & 25 deletions rust/rubydex/src/model/declaration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ macro_rules! namespace_declaration {
/// declaration inherits from
ancestors: Ancestors,
/// The set of declarations that inherit from this declaration
descendants: IdentityHashSet<DeclarationId>,
immediate_descendants: IdentityHashSet<DeclarationId>,
/// The singleton class associated with this declaration
singleton_class_id: Option<DeclarationId>,
/// Diagnostics associated with this declaration
Expand All @@ -124,7 +124,7 @@ macro_rules! namespace_declaration {
references: IdentityHashSet::default(),
owner_id,
ancestors: Ancestors::Partial(Vec::new()),
descendants: IdentityHashSet::default(),
immediate_descendants: IdentityHashSet::default(),
singleton_class_id: None,
diagnostics: Vec::new(),
}
Expand Down Expand Up @@ -206,20 +206,20 @@ macro_rules! namespace_declaration {
matches!(&self.ancestors, Ancestors::Complete(_) | Ancestors::Cyclic(_))
}

pub fn add_descendant(&mut self, descendant_id: DeclarationId) {
self.descendants.insert(descendant_id);
pub fn add_immediate_descendant(&mut self, descendant_id: DeclarationId) {
self.immediate_descendants.insert(descendant_id);
}

fn remove_descendant(&mut self, descendant_id: &DeclarationId) {
self.descendants.remove(descendant_id);
fn remove_immediate_descendant(&mut self, descendant_id: &DeclarationId) {
self.immediate_descendants.remove(descendant_id);
}

pub fn clear_descendants(&mut self) {
self.descendants.clear();
pub fn clear_immediate_descendants(&mut self) {
self.immediate_descendants.clear();
}

pub fn descendants(&self) -> &IdentityHashSet<DeclarationId> {
&self.descendants
pub fn immediate_descendants(&self) -> &IdentityHashSet<DeclarationId> {
&self.immediate_descendants
}
}
};
Expand Down Expand Up @@ -526,16 +526,16 @@ impl Namespace {
}

#[must_use]
pub fn descendants(&self) -> &IdentityHashSet<DeclarationId> {
all_namespaces!(self, it => it.descendants())
pub fn immediate_descendants(&self) -> &IdentityHashSet<DeclarationId> {
all_namespaces!(self, it => it.immediate_descendants())
}

pub fn add_descendant(&mut self, descendant_id: DeclarationId) {
all_namespaces!(self, it => it.add_descendant(descendant_id));
pub fn add_immediate_descendant(&mut self, descendant_id: DeclarationId) {
all_namespaces!(self, it => it.add_immediate_descendant(descendant_id));
}

pub fn remove_descendant(&mut self, descendant_id: &DeclarationId) {
all_namespaces!(self, it => it.remove_descendant(descendant_id));
pub fn remove_immediate_descendant(&mut self, descendant_id: &DeclarationId) {
all_namespaces!(self, it => it.remove_immediate_descendant(descendant_id));
}

pub fn for_each_ancestor<F>(&self, mut f: F)
Expand All @@ -545,19 +545,12 @@ impl Namespace {
all_namespaces!(self, it => it.ancestors().iter().for_each(&mut f));
}

pub fn for_each_descendant<F>(&self, mut f: F)
where
F: FnMut(&DeclarationId),
{
all_namespaces!(self, it => it.descendants().iter().for_each(&mut f));
}

pub fn clear_ancestors(&mut self) {
all_namespaces!(self, it => it.set_ancestors(Ancestors::Partial(vec![])));
}

pub fn clear_descendants(&mut self) {
all_namespaces!(self, it => it.clear_descendants());
pub fn clear_immediate_descendants(&mut self) {
all_namespaces!(self, it => it.clear_immediate_descendants());
}

#[must_use]
Expand Down
Loading
Loading