Skip to content

Translate self-referential singleton [] RBS sigs to WithoutRuntime sigs - #1041

Draft
paracycle wants to merge 1 commit into
mainfrom
sf/b1ce6ca0-4b11-4136-bb67-435eed6a5d4b
Draft

paracycle wants to merge 1 commit into
mainfrom
sf/b1ce6ca0-4b11-4136-bb67-435eed6a5d4b

Conversation

@paracycle

Copy link
Copy Markdown
Member

Motivation

A class can define its own singleton [] so that Foo[Bar] keeps working at runtime without T::Generic, for example after a gem drops its sorbet-runtime dependency and moves its signatures to RBS comments:

#: [StubClass]
class ClientsFactory
  class << self
    #: [U] (*Class[U]) -> Class[ClientsFactory[U]]
    def [](*_types) = self
  end
end

Translated to a runtime sig, that return type calls ClientsFactory.[] while sorbet-runtime builds the sig. The wrapped ClientsFactory.[] has to build its own sig before it runs, so evaluating the sig recurses until SystemStackError. Tapioca's RBS rewriter translates every typed file it loads, gems included, so tapioca dsl fails in any application that runs all sig blocks as soon as such a gem is loaded. We hit this with a gem bump in a large Rails app: the stack overflowed in T::Private::Methods.run_sig_block_for_key with the gem's ClientsFactory sig block on every frame.

Implementation

RBSCommentsToSorbetSigs now tracks the class/module nesting while it visits the file. When it translates a signature for a singleton [] (def self.[], or def [] inside class << self) and that signature contains a generic application of the enclosing constant (Foo[...] or a qualified A::Foo[...], anywhere in a param or return type), it marks the sig without_runtime. This works the same way method_added and singleton_method_added already do. Sorbet still checks the signature statically; at runtime it is never wrapped or evaluated.

The rule is deliberately narrow: a sig on any other method that mentions Foo[...], an instance [], a singleton [] returning another class's generic, and #: (*untyped) -> self all keep their runtime sigs. A [] defined in another file, or inside class << Other, is not detected.

Tests

  • New test_translate_to_rbi_singleton_bracket_applying_own_generic_is_without_runtime covers both class << self and def self.[] forms, an unqualified and a qualified self-reference, in both output formats. It fails without the change.
  • New test_translate_to_rbi_keeps_runtime_sigs_that_do_not_call_their_own_bracket_method covers the cases above that must keep runtime sigs.
  • End to end: translating the original gem file with this branch and then running T::Utils.run_all_sig_blocks succeeds, and ClientsFactory[Object] still returns the class. With main, the same script raises SystemStackError.
  • srb tc and RuboCop are clean on the changed files. The full suite gives the same results with and without this change (712 → 714 tests, the 2 new ones pass). The 143 failures and errors on both sides are CLI subprocess tests that can't find their gems in the environment I ran them in.

When a class defines its own singleton `[]` (for example to keep `Foo[Bar]` working after dropping `sorbet-runtime`'s `T::Generic#[]`) and its RBS signature mentions a generic application of that same class, such as `#: [U] (*Class[U]) -> Class[Foo[U]]`, the translated runtime sig calls `Foo.[]` while sorbet-runtime builds it. The wrapped `Foo.[]` must build its own sig before it runs, so evaluating the sig recurses until `SystemStackError`. Tapioca's RBS rewriter translates every typed file it loads, gems included, so `tapioca dsl` in any application that runs all sig blocks fails as soon as such a gem is loaded.

Emit `T::Sig::WithoutRuntime.sig` for these signatures, the same way `method_added` and `singleton_method_added` are already handled. Sorbet still checks them statically; at runtime they are never wrapped or evaluated.

Co-authored-by: Ufuk Kayserilioglu <ufuk.kayserilioglu@shopify.com>
@paracycle paracycle self-assigned this Oct 23, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants