From 5e9368c7ea1f036592f45ba76039f32de0dd4392 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 05:27:29 +0000 Subject: [PATCH 1/2] Skip the topological sort of methods without aliases `MethodBuilder::Methods#each` sorted the methods of every type with TSort, although the sort exists only to yield the original method of an alias before the alias, and to detect recursive aliases. Without an alias member every method is its own SCC and the sort yields them in insertion order, so check for aliases in one pass and iterate the table directly when there is none. The sort was one of the two hotspots of building the definitions of every type in an environment, next to the variance validation -- 19% of a whole-environment warmup of a large Rails application, whose methods mostly come from generated RBS files without aliases. Co-Authored-By: Claude Fable 5 --- lib/rbs/definition_builder/method_builder.rb | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/rbs/definition_builder/method_builder.rb b/lib/rbs/definition_builder/method_builder.rb index 530a9d208..163e57c18 100644 --- a/lib/rbs/definition_builder/method_builder.rb +++ b/lib/rbs/definition_builder/method_builder.rb @@ -48,12 +48,20 @@ def validate! def each if block_given? - Sorter.new(methods).each_strongly_connected_component do |scc| - if scc.size > 1 - raise RecursiveAliasDefinitionError.new(type: type, defs: scc) - end + # The topological sort exists to yield the original method of an alias first, + # and without an alias every method is its own SCC in insertion order + if methods.each_value.any? {|defn| defn.original.is_a?(AST::Members::Alias) } + Sorter.new(methods).each_strongly_connected_component do |scc| + if scc.size > 1 + raise RecursiveAliasDefinitionError.new(type: type, defs: scc) + end - yield scc[0] + yield scc[0] + end + else + methods.each_value do |defn| + yield defn + end end else enum_for :each From 56699e8fe478f5f4accb84ae74066fd090e4e2be Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 07:03:49 +0000 Subject: [PATCH 2/2] Replace the method sorter with a direct DFS `MethodBuilder::Methods#each` sorted the methods with the generic TSort when the type has an alias, paying for the enumerator and block machinery of the library on every method. The graph is trivial -- each method has at most one edge, to the original of an alias -- so a direct DFS yields the same order: originals before their aliases, everything else in insertion order. The recursive alias detection is preserved, including yielding a self-alias as is, which forms a size-1 SCC that the sorter never reported as recursive. Co-Authored-By: Claude Fable 5 --- lib/rbs/definition_builder/method_builder.rb | 51 ++++++++++---------- sig/method_builder.rbs | 17 +++---- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/lib/rbs/definition_builder/method_builder.rb b/lib/rbs/definition_builder/method_builder.rb index 163e57c18..2dc3a706b 100644 --- a/lib/rbs/definition_builder/method_builder.rb +++ b/lib/rbs/definition_builder/method_builder.rb @@ -46,48 +46,47 @@ def validate! self end - def each - if block_given? - # The topological sort exists to yield the original method of an alias first, - # and without an alias every method is its own SCC in insertion order + def each(&block) + if block + # Yields the original method of an alias before the alias, like the + # topological sort did, and detects recursive alias definitions on the way if methods.each_value.any? {|defn| defn.original.is_a?(AST::Members::Alias) } - Sorter.new(methods).each_strongly_connected_component do |scc| - if scc.size > 1 - raise RecursiveAliasDefinitionError.new(type: type, defs: scc) - end - - yield scc[0] - end - else + done = {} #: Hash[Definition, bool] + done.compare_by_identity methods.each_value do |defn| - yield defn + each_alias_first(defn, done, [], &block) end + else + methods.each_value(&block) end else enum_for :each end end - class Sorter - include TSort - - attr_reader :methods + private - def initialize(methods) - @methods = methods - end + def each_alias_first(defn, done, visiting, &block) + return if done[defn] - def tsort_each_node(&block) - methods.each_value(&block) + if visiting.any? {|other| other.equal?(defn) } + index = visiting.index {|other| other.equal?(defn) } or raise + raise RecursiveAliasDefinitionError.new(type: type, defs: visiting[index..] || raise) end - def tsort_each_child(defn) - if (member = defn.original).is_a?(AST::Members::Alias) - if old = methods[member.old_name] - yield old + if (member = defn.original).is_a?(AST::Members::Alias) + if old = methods.fetch(member.old_name, nil) + # A self alias forms a size-1 SCC that the topological sort yielded as is + unless old.equal?(defn) + visiting.push(defn) + each_alias_first(old, done, visiting, &block) + visiting.pop end end end + + done[defn] = true + yield defn end end diff --git a/sig/method_builder.rbs b/sig/method_builder.rbs index c9531ac30..e9bef0859 100644 --- a/sig/method_builder.rbs +++ b/sig/method_builder.rbs @@ -47,17 +47,14 @@ module RBS def each: () { (Definition) -> void } -> void | () -> Enumerator[Definition, void] - class Sorter - include TSort[Definition] + private - attr_reader methods: Hash[Symbol, Definition] - - def initialize: (Hash[Symbol, Definition]) -> void - - def tsort_each_node: { (Definition) -> void } -> void - - def tsort_each_child: (Definition) { (Definition) -> void } -> void - end + # Yields the definition, recursively yielding the original of an alias first + # + # Raises `RecursiveAliasDefinitionError` when the aliases form a cycle of two or + # more methods. + # + def each_alias_first: (Definition, Hash[Definition, bool] done, Array[Definition] visiting) { (Definition) -> void } -> void end attr_reader env: Environment