Skip to content

Materialize singleton classes for namespace declarations#872

Open
soutaro wants to merge 5 commits into
mainfrom
codex/materialize-singleton-ancestors
Open

Materialize singleton classes for namespace declarations#872
soutaro wants to merge 5 commits into
mainfrom
codex/materialize-singleton-ancestors

Conversation

@soutaro

@soutaro soutaro commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

This PR materializes all singleton class declarations for class and module namespaces.

This is required for descendants enumeration and for find_member_in_ancestors correctness. Without these declarations, a subclass singleton class such as Child::<Child> may not exist when Child is only defined as class Child < Parent, so inherited singleton ancestors and members cannot be found.

Impacted Ruby methods

  • Namespace#descendants now includes materialized singleton class declarations.
  • Namespace#find_member now returns members inherited through singleton class ancestors.

Other user-facing changes

  • Declaration search and completion results exclude materialized singleton class declarations, so newly materialized declarations do not appear as extra workspace symbols.

Performance impact

Because this materializes singleton classes for every namespace declaration, it can add memory use and runtime cost. In practice, the measured impact was small.

  • RSS: no measurable increase in synthetic corpus benchmarks or a large internal Ruby codebase
  • Runtime: about 1.8% slower on a large internal Ruby codebase; not measurable in synthetic corpus benchmarks

Validation

  • Confirmed the superclass-extend descendants regression fails on origin/main: Extension.descendants lacks Child::<Child>
  • Confirmed the singleton find_member regression fails on origin/main: Child::<Child> returns DeclarationNotFound
  • shadowenv exec -- cargo test -p rubydex find_member_in_ancestors_returns_inherited_singleton_member
  • shadowenv exec -- bundle exec ruby -Itest test/declaration_test.rb -n /test_find_member_returns_inherited_members_on_singleton_class/
  • shadowenv exec -- cargo test -p rubydex exact_match_empty_query_returns_all
  • shadowenv exec -- cargo test -p rubydex
  • shadowenv exec -- bundle exec rake ruby_test
  • git diff --check HEAD~2..HEAD

soutaro added 2 commits June 23, 2026 14:08
Assisted-By: devx/7c3d70e1-327c-4158-8ab5-ffccba197089
Assisted-By: devx/7c3d70e1-327c-4158-8ab5-ffccba197089
@soutaro soutaro requested a review from a team as a code owner June 23, 2026 05:11
Comment thread rust/rubydex/src/query.rs
}

fn is_searchable_declaration(declaration: &Declaration) -> bool {
!matches!(declaration, Declaration::Namespace(Namespace::SingletonClass(_)))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This simply drops all singleton class declarations from search results. For UX, we may want to keep explicit singleton class declarations from class <<, and only filter out synthetic ones.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How about we still always list single class decls from Rubydex, and we let the client to decide whether to show them?

@vinistock WDYT?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm a bit torn here. Filtering on the Rust side allows for great performance and parallelism. Additionally, the common case is almost never to search for the singleton class name, but instead the attached object name.

It's also still possible to reach the singleton class by searching for the attached object and then directly accessing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I’ll keep the code above for now. We can revisit it later if it causes another issue.

Assisted-By: devx/7c3d70e1-327c-4158-8ab5-ffccba197089
@soutaro soutaro self-assigned this Jun 23, 2026
Comment thread rust/rubydex/src/query.rs
}

fn is_searchable_declaration(declaration: &Declaration) -> bool {
!matches!(declaration, Declaration::Namespace(Namespace::SingletonClass(_)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How about we still always list single class decls from Rubydex, and we let the client to decide whether to show them?

@vinistock WDYT?

@st0012 st0012 added the bugfix A change that fixes an existing bug label Jun 24, 2026

@vinistock vinistock left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we make the move to always create singleton classes eagerly, then I think the right layer for the fix would to create a singleton class whenever the class gets created. It may require studying the impact for lazily creating singleton class of singleton classes (e.g.: Foo::<Foo>::<<Foo>>).

Also, I wonder if it would still be worth trying to minimize the amount of work. So far, if no operation in the code reaches into the singleton class, we've been considering that it doesn't exist in our modelling (lazy). Maybe we should only create singleton classes for the descendants of existing singleton classes (instead of all of them).

@soutaro

soutaro commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@vinistock

think the right layer for the fix would to create a singleton class whenever the class gets created.

It makes sense. Will fix the PR to do so.

And we still need to keep the lazy path for meta-singleton classes anyway. Ancestor/descendant enumeration will remain broken for meta-singleton classes, but that seems acceptable.

if no operation in the code reaches into the singleton class, we've been considering that it doesn't exist in our modelling

It would be possible, but it would require a significant re-architecture of the ancestor/descendant handling. I think we should keep the eager materialization strategy for this issue for now.

soutaro added 2 commits June 25, 2026 15:50
Assisted-By: devx/7c3d70e1-327c-4158-8ab5-ffccba197089
Assisted-By: devx/7c3d70e1-327c-4158-8ab5-ffccba197089
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix A change that fixes an existing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants