Bug #22475
openPrepending a second module to an already-included module duplicates it in the includer's ancestors (regression from the Bug #22257 fix)
Description
Summary¶
Since the fix for #22257 (master c59c4d717a, backported to ruby_3_4 as c8e05b5885 and to ruby_4_0 as 06611602a9), prepending a module to a module M that already has includers works correctly the first time, but every subsequent M.prepend inserts the new module twice into each includer's ancestor chain. super therefore passes through the second and later prepended modules twice.
Reproduction¶
m = Module.new
c = Class.new { include m } # include first
a = Module.new; b = Module.new; d = Module.new
m.prepend a
m.prepend b
m.prepend d
anc = c.ancestors
puts "A x#{anc.count(a)} B x#{anc.count(b)} D x#{anc.count(d)}"
Expected (Ruby 3.3.10, 3.4.10):
Actual (Ruby 3.4.11, 3.4.12, 4.0.9, 4.1.0-preview1, master cda3527671):
super really goes through the duplicated modules:
module Base; def tabs = [:info]; end
h = Module.new; h.include Base
view = Class.new { include h }
module P1; def tabs = super << :p1; end
module P2; def tabs = super << :p2; end
h.prepend P1
h.prepend P2
p view.new.tabs
# 3.4.10: [:info, :p1, :p2]
# 3.4.12: [:info, :p2, :p1, :p2] (P2#tabs is called twice)
Not affected: prepending directly to a class, M.include after M.prepend, and prepending before the module is included.
Cause¶
The fix for #22257 registers the origin iclass backfilled into each includer in the module's subclasses list (rb_module_add_to_subclasses_list(klass, origin)) so that rb_clear_method_cache can reach it. On the next rb_prepend_module, the loop over RCLASS_SUBCLASSES(klass) now also visits that origin iclass and calls include_modules_at(origin, origin, module, FALSE) on it, which inserts a second iclass of the prepended module right after the origin. The comment in the fix ("Registering during it would visit the new iclass and prepend module into it a second time") describes this for the first call; the same thing happens on every later call.
Proposed fix¶
Skip origin iclasses in that loop:
The module is already inserted between the includer's iclass and its origin when the includer's iclass is visited. With this change (patch with a regression test attached; I will also open a pull request on GitHub and link it here) the reproduction above prints A x1 B x1 D x1 on master, the #22257 reproduction still prints :hooked, and make btest / make test-all pass (38553 tests, 0 failures).
Environment¶
- ruby 3.4.12 (2026-10-23 revision 366f5fdc00) +PRISM [arm64-darwin25]
- ruby 4.1.0dev (2026-10-27T16:37:34Z master cda3527671) +PRISM [arm64-darwin25]
- macOS 26.6.2 (arm64)
Files
Updated by naopontan (naoki takaesu) 1 day ago
Pull request: https://github.com/ruby/ruby/pull/19552
It contains the same change as the attached patch (skip origin iclasses in the subclasses loop of rb_prepend_module) plus a regression test in test/ruby/test_module.rb.