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
20 changes: 20 additions & 0 deletions ext/rubydex/graph.c
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,24 @@ static VALUE rdxr_graph_set_encoding(VALUE self, VALUE encoding) {
return Qnil;
}

// Graph#without_resolution=: (Boolean) -> void
// Configures the graph to skip accumulating resolution work items
static VALUE rdxr_graph_set_without_resolution(VALUE self, VALUE without_resolution) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, this makes for a weirder API than returning the units that API consumers can just ignore.

Basically this:

g = Rubydex::Graph.new
g.without_resolution = true
g.index_workspace

vs

g = Rubydex::Graph.new
_ignored_units = g.index_workspace
# Consumers that want resolution do
# g.resolve(units)

@Morriar what do you think?

Copy link
Member Author

@st0012 st0012 Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No the idea is to pass it as constructor arg: Rubydex::Graph.new(without_resolution: true). I just haven't found a good way to set the state and past it to the Rust side.

void *graph;
TypedData_Get_Struct(self, void *, &graph_type, graph);
rdx_graph_set_without_resolution(graph, RTEST(without_resolution));
return Qnil;
}

// Graph#without_resolution?: () -> Boolean
// Returns whether the graph is configured to skip accumulating resolution work items
static VALUE rdxr_graph_without_resolution(VALUE self) {
void *graph;
TypedData_Get_Struct(self, void *, &graph_type, graph);

return rdx_graph_without_resolution(graph) ? Qtrue : Qfalse;
}

// Graph#resolve_constant: (String, Array[String]) -> Declaration?
// Runs the resolver on a single constant reference to determine what it points to
static VALUE rdxr_graph_resolve_constant(VALUE self, VALUE const_name, VALUE nesting) {
Expand Down Expand Up @@ -498,6 +516,8 @@ void rdxi_initialize_graph(VALUE mRubydex) {
rb_define_method(cGraph, "[]", rdxr_graph_aref, 1);
rb_define_method(cGraph, "search", rdxr_graph_search, 1);
rb_define_method(cGraph, "encoding=", rdxr_graph_set_encoding, 1);
rb_define_method(cGraph, "without_resolution=", rdxr_graph_set_without_resolution, 1);
rb_define_method(cGraph, "without_resolution?", rdxr_graph_without_resolution, 0);
rb_define_method(cGraph, "resolve_require_path", rdxr_graph_resolve_require_path, 2);
rb_define_method(cGraph, "require_paths", rdxr_graph_require_paths, 1);
}
5 changes: 3 additions & 2 deletions lib/rubydex/graph.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ class Graph
#: String
attr_accessor :workspace_path

#: (?workspace_path: String) -> void
def initialize(workspace_path: Dir.pwd)
#: (?workspace_path: String, ?without_resolution: bool) -> void
def initialize(workspace_path: Dir.pwd, without_resolution: false)
@workspace_path = workspace_path
self.without_resolution = without_resolution
end

# Index all files and dependencies of the workspace that exists in `@workspace_path`
Expand Down
2 changes: 1 addition & 1 deletion rust/rubydex-mcp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl RubydexServer {
}

let mut resolver = rubydex::resolution::Resolver::new(&mut graph);
resolver.resolve_all();
resolver.resolve();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to rename this, can we move it to a separate PR? This is a complex change, so I'm trying to make sure we're focused on the core aspects of the implementation and that we lower the barrier for better reviews.


eprintln!(
"Rubydex indexed {} files, {} declarations",
Expand Down
26 changes: 24 additions & 2 deletions rust/rubydex-sys/src/graph_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ pub unsafe extern "C" fn rdx_graph_delete_document(pointer: GraphPointer, uri: *
pub extern "C" fn rdx_graph_resolve(pointer: GraphPointer) {
with_mut_graph(pointer, |graph| {
let mut resolver = Resolver::new(graph);
resolver.resolve_all();
resolver.resolve();
});
}

Expand Down Expand Up @@ -221,6 +221,28 @@ pub unsafe extern "C" fn rdx_graph_set_encoding(pointer: GraphPointer, encoding_
true
}

/// Configures the graph to skip accumulating resolution work items.
///
/// # Safety
///
/// Expects the graph pointer to be valid
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rdx_graph_set_without_resolution(pointer: GraphPointer, without_resolution: bool) {
with_mut_graph(pointer, |graph| {
graph.set_without_resolution(without_resolution);
});
}

/// Returns whether the graph is configured to skip accumulating resolution work items.
///
/// # Safety
///
/// Expects the graph pointer to be valid
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rdx_graph_without_resolution(pointer: GraphPointer) -> bool {
with_graph(pointer, |graph| graph.without_resolution())
}

/// Creates a new iterator over declaration IDs by snapshotting the current set of IDs.
///
/// # Safety
Expand Down Expand Up @@ -577,7 +599,7 @@ mod tests {
let mut graph = Graph::new();
graph.update(indexer.local_graph());
let mut resolver = Resolver::new(&mut graph);
resolver.resolve_all();
resolver.resolve();

assert_eq!(
1,
Expand Down
1 change: 1 addition & 0 deletions rust/rubydex/src/indexing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ pub fn index_source(graph: &mut Graph, uri: &str, source: &str, language_id: &La
}

/// Indexes the given paths, reading the content from disk and populating the given `Graph` instance.
/// Pending work is accumulated on the graph; drained by the resolver.
///
/// # Panics
///
Expand Down
30 changes: 30 additions & 0 deletions rust/rubydex/src/indexing/local_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::collections::hash_map::Entry;
use crate::diagnostic::{Diagnostic, Rule};
use crate::model::definitions::Definition;
use crate::model::document::Document;
use crate::model::graph::NameDependent;
use crate::model::identity_maps::IdentityHashMap;
use crate::model::ids::{DefinitionId, NameId, ReferenceId, StringId, UriId};
use crate::model::name::{Name, NameRef};
Expand All @@ -18,6 +19,7 @@ type LocalGraphParts = (
IdentityHashMap<NameId, NameRef>,
IdentityHashMap<ReferenceId, ConstantReference>,
IdentityHashMap<ReferenceId, MethodRef>,
IdentityHashMap<NameId, Vec<NameDependent>>,
);

#[derive(Debug)]
Expand All @@ -29,6 +31,7 @@ pub struct LocalGraph {
names: IdentityHashMap<NameId, NameRef>,
constant_references: IdentityHashMap<ReferenceId, ConstantReference>,
method_references: IdentityHashMap<ReferenceId, MethodRef>,
name_dependents: IdentityHashMap<NameId, Vec<NameDependent>>,
}

impl LocalGraph {
Expand All @@ -42,6 +45,7 @@ impl LocalGraph {
names: IdentityHashMap::default(),
constant_references: IdentityHashMap::default(),
method_references: IdentityHashMap::default(),
name_dependents: IdentityHashMap::default(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Populating and managing the name_dependents can be a separate PR too.

}
}

Expand Down Expand Up @@ -70,6 +74,10 @@ impl LocalGraph {
pub fn add_definition(&mut self, definition: Definition) -> DefinitionId {
let definition_id = definition.id();

if let Some(name_id) = definition.name_id() {
self.insert_name_dependent(*name_id, NameDependent::Definition(definition_id));
}

if self.definitions.insert(definition_id, definition).is_some() {
debug_assert!(false, "DefinitionId collision in local graph");
}
Expand Down Expand Up @@ -133,6 +141,7 @@ impl LocalGraph {

pub fn add_constant_reference(&mut self, reference: ConstantReference) -> ReferenceId {
let reference_id = reference.id();
self.insert_name_dependent(*reference.name_id(), NameDependent::Reference(reference_id));

if self.constant_references.insert(reference_id, reference).is_some() {
debug_assert!(false, "ReferenceId collision in local graph");
Expand Down Expand Up @@ -172,6 +181,26 @@ impl LocalGraph {
self.document.add_diagnostic(diagnostic);
}

// Name dependents

/// Registers a dependent under its own `name_id` and under the `nesting`/`parent_scope`
/// of that name, so invalidation can trace from any of those scopes.
fn insert_name_dependent(&mut self, name_id: NameId, dep: NameDependent) {
self.name_dependents.entry(name_id).or_default().push(dep);

if let Some(name_ref) = self.names.get(&name_id) {
for owner_id in [name_ref.nesting().as_ref().copied(), name_ref.parent_scope().as_ref().copied()]
.into_iter()
.flatten()
{
let deps = self.name_dependents.entry(owner_id).or_default();
if !deps.contains(&dep) {
deps.push(dep);
}
}
}
}

// Into parts

#[must_use]
Expand All @@ -184,6 +213,7 @@ impl LocalGraph {
self.names,
self.constant_references,
self.method_references,
self.name_dependents,
)
}
}
2 changes: 1 addition & 1 deletion rust/rubydex/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ fn main() {

time_it!(resolution, {
let mut resolver = Resolver::new(&mut graph);
resolver.resolve_all();
resolver.resolve();
});

if let Some(StopAfter::Resolution) = args.stop_after {
Expand Down
4 changes: 4 additions & 0 deletions rust/rubydex/src/model/declaration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,10 @@ impl Namespace {
all_namespaces!(self, it => it.member(str_id))
}

pub fn remove_member(&mut self, str_id: &StringId) -> Option<DeclarationId> {
all_namespaces!(self, it => it.remove_member(str_id))
}

#[must_use]
pub fn singleton_class(&self) -> Option<&DeclarationId> {
all_namespaces!(self, it => it.singleton_class_id())
Expand Down
Loading
Loading