Issue #22178 has been reported by watson1978 (Shizuo Fujita). ---------------------------------------- Bug #22178: Regexp#== regression: regexps built via the `rb_reg_new` C API are no longer equal to an identical literal https://bugs.ruby-lang.org/issues/22178 * Author: watson1978 (Shizuo Fujita) * Status: Open * ruby -v: ruby 4.1.0dev (2026-06-24T22:57:54Z master d663de3fc4) +PRISM [x86_64-linux] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- On current master, two `Regexp` objects that have the same options, the same regexp encoding (`Regexp#encoding`), and byte-identical sources are no longer `==` if their *source strings* have different encodings (e.g. `ASCII-8BIT` vs `US-ASCII`). This worked on 4.0 and earlier. Such a regexp is produced by the public C API `rb_reg_new()`, which does not normalize the source encoding, so an ASCII-only pattern keeps an `ASCII-8BIT` source instead of being promoted to `US-ASCII`. ## Reproduction ```ruby require "fiddle" libruby = Fiddle.dlopen(nil) RB_REG_NEW = Fiddle::Function.new( libruby["rb_reg_new"], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_INT], Fiddle::TYPE_VOIDP ) def reg_via_c_api(src, options = 0) # what a C extension using ptr = RB_REG_NEW.call(Fiddle::Pointer[src], src.bytesize, options) Fiddle.dlunwrap(ptr.to_i) # rb_reg_new() produces end c = reg_via_c_api("^[0-9]") # rb_reg_new() -> source ASCII-8BIT lit = /^[0-9]/ # literal -> source US-ASCII p c.source.encoding # ASCII-8BIT p lit.source.encoding # US-ASCII p c.encoding # US-ASCII p lit.encoding # US-ASCII p(c.options == lit.options) # true p(c.source == lit.source) # true (byte-identical) p(c == lit) # 4.0: true / master: false ``` Output on master: ``` #<Encoding:ASCII-8BIT> #<Encoding:US-ASCII> #<Encoding:US-ASCII> #<Encoding:US-ASCII> true true false # <-- was true on 4.0.5 ``` ## Cause Bisects to commit **d663de3fc43a8bb54bbdde12764b54bff4369da0** ("re.c: Simplify rb_reg_equal", 2026-06-24). Confirmed by building the two adjacent commits. | commit | `c == lit` | |--------|-----------| | `bb75c2893a` (parent) | `true` | | `d663de3fc4` | `false` | `rb_reg_equal` changed from comparing regexp encoding + source **content** to comparing the source **fstring identity**: ```c // before if (RREGEXP_SRC_LEN(re1) != RREGEXP_SRC_LEN(re2)) return Qfalse; if (ENCODING_GET(re1) != ENCODING_GET(re2)) return Qfalse; return RBOOL(memcmp(RREGEXP_SRC_PTR(re1), RREGEXP_SRC_PTR(re2), RREGEXP_SRC_LEN(re1)) == 0); // after if (RREGEXP_SRC(re1) != RREGEXP_SRC(re2)) return Qfalse; // fstring pointer compare return RBOOL(ENCODING_GET(re1) == ENCODING_GET(re2)); ``` Because fstring interning is encoding-sensitive, a `US-ASCII` `"^[0-9]"` and an `ASCII-8BIT` `"^[0-9]"` are distinct fstrings, so the new pointer comparison returns `false` where the old content+encoding comparison returned `true`. ## Question Is this behavior change intentional, or a side effect of the optimization? The commit message describes d663de3fc4 as a pure optimization and doesn't mention making the source string's encoding significant to Regexp#==, so it reads like an unintended side effect: the optimization assumes every regexp source is a normalized fstring, but rb_reg_new() can produce one whose source is a valid fstring with a non-normalized (`ASCII-8BIT`) encoding. -- https://bugs.ruby-lang.org/