Project

General

Profile

Actions

Bug #21529

open

Deprecate the /o modifier and warn against using it

Added by jpcamara (JP Camara) 1 day ago. Updated about 1 hour ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:122900]

Description

I recently ran into a bug in some code because it was using the /o modifier as an optimization, not realizing it created a permanent, immutable value after the first time it gets evaluated. I dug into how the modifier works in CRuby and the history of it here: https://jpcamara.com/2025/08/02/the-o-in-ruby-regex.html.

The feature seems like a total footgun with almost no upside. If I run a benchmark between a local regex, and a regex cached by /o, there is no real difference.

require "benchmark"

def letters
  "A-Za-z"
end

words = %w[the quick brown fox jumped over the lazy dog]

Benchmark.bm do |bm|
  bm.report("without /o:") do
    regex = /\A[A-Za-z]+\z/
    words.each do |word|
      word.match(regex)
    end
  end

  bm.report("with /o:   ") do
    words.each do |word|
      word.match(/\A[#{letters}]+\z/o)
    end
  end
end

Most of the time I found that "without /o" actually came out ahead.

                 user     system      total        real
without /o:  0.000019   0.000003   0.000022 (  0.000014)
with /o:     0.000020   0.000001   0.000021 (  0.000020)

I'd like to deprecate the feature and update the docs to warn against using it. I'd be happy to submit a PR doing that.

Thanks!

Updated by jpcamara (JP Camara) 1 day ago

Byroot brought to my attention that my example doesn’t make a lot of sense because it doesn’t interpolate anything.

I’m hard pressed to find an example to compare with dynamic interpolation, since I think that the core issue with /o is that dynamic interpolation doesn’t work the way anyone would ever expect.

But here’s an example that precompiles a regex, which I think is the only comparison that is apples to apples, at its core (no pun intended)


require "benchmark"

def letters
  "A-Za-z"
end

words = %w[the quick brown fox jumped over the lazy dog]
PRECOMPILED = /\A[#{letters}]+\z/.freeze

Benchmark.bm do |bm|
  bm.report("without /o:") do
    words.each do |word|
      word.match(PRECOMPILED)
    end
  end

  bm.report("with /o:   ") do
    words.each do |word|
      word.match(/\A[#{letters}]+\z/o)
    end
  end
end

The performance is the same again, but at least the example is slightly more relevant.

Updated by byroot (Jean Boussier) 1 day ago

My point was that /o is only "optimized" compared to the same interpolation but uncached, so:


require "benchmark"

def letters
  "A-Za-z"
end

words = %w[the quick brown fox jumped over the lazy dog]

Benchmark.bm do |bm|
  bm.report("without /o:") do
    words.each do |word|
      word.match(/\A[#{letters}]+\z/)
    end
  end

  bm.report("with /o:   ") do
    words.each do |word|
      word.match(/\A[#{letters}]+\z/o)
    end
  end
end

But yes, in the overwhelming majority of cases, you are much better to explicitly only performance the interpolation once and store the resulting regexp in a constant.

/o is definitely a footgun, but also a very rare construct. In my opinion improving the documentation is more than welcome, but I'm not convinced deprecating is worth it, as I assume the problems come from people seeing it in the docs and misunderstanding the docs.

Updated by jpcamara (JP Camara) 1 day ago

Yea I hear ya. So should I just submit a PR with my suggestions for the docs and close this?

Updated by byroot (Jean Boussier) about 8 hours ago

So should I just submit a PR with my suggestions for the docs

Sure. I think updating the code snippets, and suggesting to use a constant could go a long way.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0