<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.6.2">Jekyll</generator><link href="https://eregon.me/blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://eregon.me/blog/" rel="alternate" type="text/html" /><updated>2026-09-15T12:59:09+02:00</updated><id>https://eregon.me/blog/</id><title type="html">On the Edge of Ruby</title><subtitle>A blog about Ruby, Performance and Concurrency</subtitle><author><name>Benoit Daloze</name></author><entry><title type="html">Contributions to ruby/spec</title><link href="https://eregon.me/blog/2025/05/03/contributions-to-ruby-spec.html" rel="alternate" type="text/html" title="Contributions to ruby/spec" /><published>2025-05-03T00:00:00+02:00</published><updated>2025-05-03T00:00:00+02:00</updated><id>https://eregon.me/blog/2025/05/03/contributions-to-ruby-spec</id><content type="html" xml:base="https://eregon.me/blog/2025/05/03/contributions-to-ruby-spec.html">&lt;p&gt;As a maintainer of ruby/spec I sometimes wonder how much each Ruby implementation contributes to ruby/spec.
Without further ado, here it is:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/rubyspec-contribs.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;interpretation&quot;&gt;Interpretation&lt;/h2&gt;

&lt;p&gt;Rubinius started ruby/spec (called RubySpec back then) and was the main contributor until 2014.
In 2015, Rubinius development &lt;a href=&quot;https://github.com/rubinius/rubinius/graphs/contributors&quot;&gt;slowed down significantly&lt;/a&gt; and so did their work on adding specs (more on that in this &lt;a href=&quot;/blog/2015/07/29/rubyspec-is-reborn.html&quot;&gt;10 years old post&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In 2014, TruffleRuby was started and quickly became the main contributor to ruby/spec, contributing more than half of all commits since then.&lt;/p&gt;

&lt;p&gt;CRuby has contributed consistently to ruby/spec, around 20% of all commits each year.
This is great to see as CRuby is in the unique position of implementing new methods first and deciding the expected behavior.&lt;/p&gt;

&lt;p&gt;JRuby contributed significantly until 2012 but since then has made few contributions.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://natalie-lang.org/&quot;&gt;Natalie&lt;/a&gt;, a Ruby implementation started in 2019 made significant contributions since 2022, big thanks to &lt;a href=&quot;https://github.com/herwinw&quot;&gt;@herwinw&lt;/a&gt; which contributed most of that.&lt;/p&gt;

&lt;p&gt;External contributors have always been an important part of ruby/spec.
This chart might unfortunately have slightly lower numbers for external contributors due to the GitHub API only providing contributor statistics for the top 100 contributors (see below for more details on that).&lt;/p&gt;

&lt;p&gt;You might notice the huge spike for external contributors in 2009.
This is because &lt;a href=&quot;https://github.com/runpaint&quot;&gt;@runpaint&lt;/a&gt; contributed &lt;a href=&quot;https://github.com/eregon/rubyspec-stats/blob/92298be1469b2640f24a1f681a6d984634bf51ef/contributors/per_year.yml#L23&quot;&gt;1145 commits&lt;/a&gt; that year, while working on migrating specs from Ruby 1.8 to Ruby 1.9 and adding many new specs.&lt;/p&gt;

&lt;h2 id=&quot;before-2008&quot;&gt;Before 2008&lt;/h2&gt;

&lt;p&gt;The first specs were actually added directly in the Rubinius repository, &lt;a href=&quot;https://github.com/ruby/spec/blob/2b886623f7589bc01dcda5efd4b59abba269e264/CHANGES.before-2008-05-10#L17794&quot;&gt;in 2006&lt;/a&gt;.
Commits between 2006 and 9 May 2008 are recorded in &lt;a href=&quot;https://github.com/ruby/spec/blob/2b886623f7589bc01dcda5efd4b59abba269e264/CHANGES.before-2008-05-10&quot;&gt;a changelog file&lt;/a&gt; but are not part of the ruby/spec repository history. Basically, before May 2008 everything was contributed through the Rubinius repository.
Also bear in mind that the data for the chart above is incomplete for the first and last years.&lt;/p&gt;

&lt;h2 id=&quot;methodology&quot;&gt;Methodology&lt;/h2&gt;

&lt;p&gt;If one want to see contributions by individual author instead, that is &lt;a href=&quot;https://github.com/ruby/spec/graphs/contributors&quot;&gt;available directly on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The data is computed by &lt;a href=&quot;https://github.com/eregon/rubyspec-stats/blob/master/contributors/contribs.rb&quot;&gt;this script&lt;/a&gt; and the chart is generated by &lt;a href=&quot;https://github.com/eregon/rubyspec-stats/blob/master/contributors/chart.py&quot;&gt;this script&lt;/a&gt;.
You can find all computed data files in &lt;a href=&quot;https://github.com/eregon/rubyspec-stats/blob/master/contributors/&quot;&gt;this directory&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The GitHub API only provides contributor statistics for the top 100 contributors, so everything is based on that data.
The top 100 contributors to ruby/spec amounts to 13069 commits, out of 15045 commits total.&lt;/p&gt;

&lt;p&gt;To classify authors by Ruby implementation, I used the top 100 contributors of each Ruby implementation.
If the author contributed at least 10 commit to a Ruby implementation and is in the top 100 contributors the script considers them as contributing to ruby/spec through that Ruby implementation.
If the author contributed to more than one Ruby implementation, I just pick the one they contributed the most commits to.
This is not perfect but it seems &lt;a href=&quot;https://github.com/eregon/rubyspec-stats/blob/master/contributors/mapping.yml&quot;&gt;reasonably accurate&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As always, commits are not a perfect measure of how much was contributed, but seems the best estimate we have readily available.&lt;/p&gt;

&lt;h2 id=&quot;more-charts&quot;&gt;More Charts&lt;/h2&gt;

&lt;p&gt;For curiosity, I also redid the chart with a minimum of 100 commits to classify authors as contributing through a Ruby implementation (to only count key contributors to a Ruby implementation as such and classify the rest as external contributors).
The result looks similar, though with more external contributors as expected:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/rubyspec-contribs-100commits.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I also did a chart with the absolute numbers of commits instead of relative to the number of commits per year. The commits for 2025 are fewer because we are only in May. This chart uses the 10 commits limit as the original chart to classify per Ruby implementation.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/rubyspec-contribs-absolute.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;ruby/spec is key to implementing Ruby correctly and efficiently.
It provides extensive coverage of Ruby’s behavior, far more precise than the documentation.
ruby/spec has always relied on external contributions, from individuals and from contributors to alternative Ruby implementations.
Contributing to ruby/spec is contributing to making Ruby better specified, better tested and helping Ruby implementations to be more compatible.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/ruby/ruby/tree/master/test/ruby&quot;&gt;CRuby test suite&lt;/a&gt;, while extensive and valuable, is unfortunately in my experience not enough to implement Ruby correctly based on it alone.
A clear indication of that is the hundreds of CRuby issues (inconsistencies, segfaults, incorrect behaviors, etc) reported and discovered while writing ruby/spec specs.&lt;/p&gt;

&lt;p&gt;My intuition there is the structure of ruby/spec specs (nested &lt;code class=&quot;highlighter-rouge&quot;&gt;describe&lt;/code&gt;s, etc) encourages to test all representative behaviors such as e.g. the various combinations of passing different argument types to some parameters together with passing a block or not, etc.
OTOH unit tests as done in the CRuby test suite typically consist of a few assertions within a single &lt;code class=&quot;highlighter-rouge&quot;&gt;test_foo&lt;/code&gt; method. I think having everything in a single test method discourages to write many assertions (to avoid too long methods) and often means there are only a few edge cases or basic cases being tested.
In addition, there is no descriptive text explaining what is being tested so one needs to reverse engineer what an assertion is supposed to test every time one looks at such a test.
I am thinking to potentially pick 10 random methods and compare their specs &amp;amp; tests to figure out how much this holds in a future blog post.&lt;/p&gt;</content><author><name>Benoit Daloze</name></author><summary type="html">As a maintainer of ruby/spec I sometimes wonder how much each Ruby implementation contributes to ruby/spec. Without further ado, here it is:</summary></entry><entry><title type="html">Matching Regexps 200 Times Faster</title><link href="https://eregon.me/blog/2025/03/14/matching-regexps-200-times-faster.html" rel="alternate" type="text/html" title="Matching Regexps 200 Times Faster" /><published>2025-03-14T00:00:00+01:00</published><updated>2025-03-14T00:00:00+01:00</updated><id>https://eregon.me/blog/2025/03/14/matching-regexps-200-times-faster</id><content type="html" xml:base="https://eregon.me/blog/2025/03/14/matching-regexps-200-times-faster.html">&lt;p&gt;You might have seen &lt;a href=&quot;https://byroot.github.io/ruby/json/2024/12/15/optimizing-ruby-json-part-1.html&quot;&gt;@byroot’s  excellent blog post series on optimizing the &lt;code class=&quot;highlighter-rouge&quot;&gt;json&lt;/code&gt; gem&lt;/a&gt;.
From the first blog post it’s clear most of the time for generating JSON is spent in &lt;code class=&quot;highlighter-rouge&quot;&gt;generate_json_string()&lt;/code&gt; and specifically in &lt;code class=&quot;highlighter-rouge&quot;&gt;convert_UTF8_to_JSON()&lt;/code&gt;, i.e., in converting Ruby Strings to JSON Strings.&lt;/p&gt;

&lt;p&gt;Because this is such a hot part of JSON generation, it has received a lot of attention.
In part 3 @byroot shows &lt;a href=&quot;https://byroot.github.io/ruby/json/2024/12/27/optimizing-ruby-json-part-3.html#path-splitting&quot;&gt;a lookup table approach&lt;/a&gt; to implement &lt;code class=&quot;highlighter-rouge&quot;&gt;convert_UTF8_to_JSON()&lt;/code&gt;, which is now used in the &lt;code class=&quot;highlighter-rouge&quot;&gt;json&lt;/code&gt; gem.&lt;/p&gt;

&lt;p&gt;There has been &lt;a href=&quot;https://github.com/ruby/json/pull/730&quot;&gt;a couple&lt;/a&gt; &lt;a href=&quot;https://github.com/ruby/json/pull/743&quot;&gt;attempts&lt;/a&gt; since then to optimize it further using SIMD instructions.
This is however quite difficult and messy to do in C as it’s similar to writing inline assembly (N times, with N the number of SIMD variants), and it’s not portable so it needs both compile-time and run-time detection to pick the correct SIMD variant.&lt;/p&gt;

&lt;p&gt;In this blog post we compare 3 contenders for the job of converting Ruby Strings to JSON Strings.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/ruby/json/blob/c472d72fdf1fd8e297cfdaf05b95b1ddcfef63fb/ext/json/ext/generator/generator.c#L106-L191&quot;&gt;The C extension code used in ruby/json&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/samyron/json/blob/b98ab4037e65aae9773f336e6f4fb9061be68fc6/ext/json/ext/generator/generator.c#L561-L684&quot;&gt;The C extension code + AVX2 SIMD&lt;/a&gt; part of this &lt;a href=&quot;https://github.com/ruby/json/pull/730&quot;&gt;SIMD PR&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/ruby/json/blob/c472d72fdf1fd8e297cfdaf05b95b1ddcfef63fb/lib/json/truffle_ruby/generator.rb#L60-L61&quot;&gt;A pure-Ruby version&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pure-Ruby version is so simple it fits in a few lines (and a big &lt;code class=&quot;highlighter-rouge&quot;&gt;Hash&lt;/code&gt; literal):&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;ESCAPE_MAP&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0000'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0001'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x2&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0002'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x3&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0003'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x4&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0004'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x5&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0005'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x6&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0006'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x7&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0007'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\b&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\b'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\t&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\t'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\n'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\xb&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u000b'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\f'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\r&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\r'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\xe&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u000e'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\xf&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u000f'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x10&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0010'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x11&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0011'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x12&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0012'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x13&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0013'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x14&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0014'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x15&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0015'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x16&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0016'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x17&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0017'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x18&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0018'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x19&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u0019'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x1a&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u001a'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x1b&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u001b'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x1c&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u001c'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x1d&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u001d'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x1e&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u001e'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x1f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\u001f'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s1&quot;&gt;'&quot;'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\&quot;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\\'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'\\\\'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;utf8_to_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;s1&quot;&gt;'&quot;'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/[&quot;\\\x0-\x1f]/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;match?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;gsub&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/[&quot;\\\x0-\x1f]/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ESCAPE_MAP&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;string&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'&quot;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;ESCAPE_MAP&lt;/code&gt; is a bit verbose but it’s a simple way to exhaustively store how to escape each character which needs to be escaped.
The &lt;code class=&quot;highlighter-rouge&quot;&gt;match?&lt;/code&gt; is not strictly necessary but it avoids the allocation of a new String for the common case of no character to escape.&lt;/p&gt;

&lt;p&gt;We use &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby/json&lt;/code&gt;’s &lt;a href=&quot;https://github.com/ruby/json/blob/c472d72fdf1fd8e297cfdaf05b95b1ddcfef63fb/benchmark/encoder.rb#L76-L79&quot;&gt;string encoding benchmarks&lt;/a&gt; with a small diff to run only those:&lt;/p&gt;
&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;diff --git a/benchmark/encoder.rb b/benchmark/encoder.rb
index f0a05db..efbac2b 100644
&lt;/span&gt;&lt;span class=&quot;gd&quot;&gt;--- a/benchmark/encoder.rb
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+++ b/benchmark/encoder.rb
&lt;/span&gt;&lt;span class=&quot;gu&quot;&gt;@@ -33,6 +33,8 @@ def implementations(ruby_obj)
&lt;/span&gt; end
 
 def benchmark_encoding(benchmark_name, ruby_obj, check_expected: true, except: [])
&lt;span class=&quot;gi&quot;&gt;+  return unless [&quot;mixed utf8&quot;, &quot;mostly utf8&quot;].include?(benchmark_name)
&lt;/span&gt;   json_output = JSON.dump(ruby_obj)
   puts &quot;== Encoding #{benchmark_name} (#{json_output.bytesize} bytes)&quot;
 
&lt;span class=&quot;gu&quot;&gt;@@ -40,6 +42,7 @@ def benchmark_encoding(benchmark_name, ruby_obj, check_expected: true, except: [
&lt;/span&gt;   except.each { |i| impls.delete(i) }
 
   Benchmark.ips do |x|
&lt;span class=&quot;gi&quot;&gt;+    x.warmup = 5
&lt;/span&gt;     expected = ::JSON.dump(ruby_obj) if check_expected
     impls.values.each do |name, block|
       begin
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;We select the benchmarks we want with an early return and we increase warmup to increase the stability of the benchmark results.
We use &lt;code class=&quot;highlighter-rouge&quot;&gt;ONLY=json&lt;/code&gt; because the SIMD PR doesn’t have the &lt;code class=&quot;highlighter-rouge&quot;&gt;json_coder&lt;/code&gt; variant (anyway both of these give very similar results for the selected benchmarks).&lt;/p&gt;

&lt;h2 id=&quot;json-string-escaping-benchmark&quot;&gt;JSON String Escaping Benchmark&lt;/h2&gt;

&lt;p&gt;The benchmarks are straightforward:&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# mixed utf8&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;a&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;€&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;a&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# mostly utf8&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;€&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3333&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;They both dump to JSON an Array of 500 strings and end up generating a JSON result of 5MB.&lt;/p&gt;

&lt;p&gt;Here are the results on my machine (&lt;code class=&quot;highlighter-rouge&quot;&gt;AMD Ryzen 7 3700X 8-Core Processor&lt;/code&gt;), with frequency scaling disabled and the &lt;code class=&quot;highlighter-rouge&quot;&gt;performance&lt;/code&gt; CPU governor:&lt;/p&gt;

&lt;p&gt;C extension with CRuby 3.4.2 with YJIT (on &lt;a href=&quot;https://github.com/ruby/json/commit/c472d72fdf1fd8e297cfdaf05b95b1ddcfef63fb&quot;&gt;ruby/json master&lt;/a&gt;):&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ ONLY=json ruby --yjit -Ilib:ext benchmark/encoder.rb
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT +PRISM [x86_64-linux]
mixed utf8    397.953 (± 4.5%) i/s    (2.51 ms/i)
mostly utf8   402.388 (± 0.5%) i/s    (2.49 ms/i)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We will use this as the baseline, specifically the &lt;code class=&quot;highlighter-rouge&quot;&gt;mixed utf8&lt;/code&gt; benchmark to keep things simple.&lt;/p&gt;

&lt;p&gt;C extension + SIMD with CRuby 3.4.2 with YJIT (on the &lt;a href=&quot;https://github.com/ruby/json/pull/730&quot;&gt;SIMD PR&lt;/a&gt;):&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ ONLY=json ruby --yjit -Ilib:ext benchmark/encoder.rb
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT +PRISM [x86_64-linux]
mixed utf8      1.498k (± 4.0%) i/s  (667.68 μs/i)
mostly utf8     1.474k (± 1.6%) i/s  (678.55 μs/i)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;3.76&lt;/strong&gt; times faster, nice!&lt;/p&gt;

&lt;p&gt;Pure-Ruby generator with TruffleRuby 24.1.1 JVM (on &lt;a href=&quot;https://github.com/ruby/json/commit/c472d72fdf1fd8e297cfdaf05b95b1ddcfef63fb&quot;&gt;ruby/json master&lt;/a&gt;):&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ ONLY=json ruby -Ilib:ext benchmark/encoder.rb
truffleruby 24.1.1, like ruby 3.2.4, Oracle GraalVM JVM [x86_64-linux]
mixed utf8      8.104k (± 0.6%) i/s  (123.40 μs/i)
mostly utf8     8.070k (± 1.1%) i/s  (123.91 μs/i)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;20&lt;/strong&gt; times faster than the baseline and &lt;strong&gt;5.4&lt;/strong&gt; times faster than SIMD!&lt;br /&gt;
And, of course much simpler, this is just a few lines of Ruby code and a Regexp.&lt;/p&gt;

&lt;p&gt;If you guessed earlier that the C extension code + SIMD would be fastest, you may be surprised to learn the pure-Ruby version is considerably faster (on TruffleRuby)!
That’s the benefit of an advanced JIT compiler which understands Ruby semantics like TruffleRuby’s JIT compiler (called Graal): you get to write nice, idiomatic Ruby and the JIT compiler does the hard work of optimizing it for your target platform.&lt;/p&gt;

&lt;p&gt;For comparison, here’s the pure-Ruby generator with CRuby 3.4.2 with YJIT (on &lt;a href=&quot;https://github.com/ruby/json/commit/c472d72fdf1fd8e297cfdaf05b95b1ddcfef63fb&quot;&gt;ruby/json master&lt;/a&gt;):&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ ONLY=json ruby --yjit -Ilib:ext benchmark/encoder.rb
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT +PRISM [x86_64-linux]
mixed utf8     39.551 (± 0.0%) i/s   (25.28 ms/i)
mostly utf8    28.119 (± 0.0%) i/s   (35.56 ms/i)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Unfortunately, it’s 10 times slower than the baseline, demonstrating the Regexp approach is much slower on CRuby.
But, we ran with YJIT enabled, so why is it so much slower?&lt;/p&gt;

&lt;p&gt;As a disclaimer, TruffleRuby (which uses the pure-Ruby generator) is currently slower than CRuby using the C extension
on &lt;code class=&quot;highlighter-rouge&quot;&gt;JSON.generate&lt;/code&gt; macro benchmarks (except &lt;code class=&quot;highlighter-rouge&quot;&gt;canada.json&lt;/code&gt; where it is faster).
Other aspects of JSON generation need to be better optimized on TruffleRuby.&lt;/p&gt;

&lt;h2 id=&quot;regexpmatch-benchmark&quot;&gt;Regexp#match? Benchmark&lt;/h2&gt;

&lt;p&gt;Let’s simplify the benchmark to just &lt;code class=&quot;highlighter-rouge&quot;&gt;Regexp#match?&lt;/code&gt; to understand better:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'benchmark/ips'&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;mixed_utf8&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;a&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;€&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;a&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Benchmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ips&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;warmup&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'/[&quot;\\\x0-\x1f]/.match?(mixed_utf8)'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/[&quot;\\\x0-\x1f]/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;match?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mixed_utf8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ ruby --yjit bench_regexp_match.rb
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT +PRISM [x86_64-linux]
20.700k (± 2.4%) i/s   (48.31 μs/i)

$ ruby bench_regexp_match.rb
truffleruby 24.1.1, like ruby 3.2.4, Oracle GraalVM JVM [x86_64-linux]
 4.699M (± 0.3%) i/s  (212.83 ns/i)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;TruffleRuby is &lt;strong&gt;227&lt;/strong&gt; times faster than CRuby at Regexp matching on this benchmark!
TruffleRuby matches 10003 bytes in 213 nanoseconds, that is 47 bytes per nanosecond, which is greater than the number of CPU cycles we have per nanosecond (this processor has a max frequency of 4.4 GHz), so what’s going on here?&lt;/p&gt;

&lt;p&gt;How can Regexp matching be so fast on TruffleRuby?&lt;/p&gt;

&lt;p&gt;There are multiple reasons:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Regexps on TruffleRuby use &lt;a href=&quot;https://github.com/oracle/graal/blob/master/regex/README.md&quot;&gt;TRegex&lt;/a&gt;, a state-of-the-art Regular Expression Engine:&lt;/li&gt;
  &lt;li&gt;TRegex compiles regexps to &lt;a href=&quot;https://swtch.com/~rsc/regexp/regexp1.html&quot;&gt;finite-state automata&lt;/a&gt;, meaning it never goes back in the input string and will read each character at most once. Therefore, &lt;em&gt;it always runs in linear time&lt;/em&gt;. CRuby uses backtracking which may go over the input string characters many times, which is much slower.&lt;/li&gt;
  &lt;li&gt;Backtracking is prone to ReDoS when backtracking too much while &lt;em&gt;finite-state automata is immune to ReDoS&lt;/em&gt;. CRuby 3.2+ has a protection for most regexps against ReDoS by using some form of caching. While that caching avoids many ReDoS, it also slows down the regexp execution by needing to check or fill this cache every time it might backtrack.&lt;/li&gt;
  &lt;li&gt;TRegex &lt;em&gt;JIT-compiles regexps&lt;/em&gt; to native code. CRuby compiles the regexp to regexp bytecode and then uses an interpreter to run that bytecode. That means it has dispatch overhead, does not optimize across bytecodes, etc. YJIT does not know anything about that regexp interpreter, it sees it as a blackbox because it is all written in C.&lt;/li&gt;
  &lt;li&gt;TruffleRuby uses &lt;a href=&quot;https://rubykaigi.org/2023/presentations/eregontp.html&quot;&gt;splitting&lt;/a&gt; so that each call site for &lt;code class=&quot;highlighter-rouge&quot;&gt;Regexp#match?&lt;/code&gt; and other Regexp-related methods has its own copy of the logic. With that, TruffleRuby is able to &lt;em&gt;inline&lt;/em&gt; the JIT-compiled code for that specific Regexp in the Ruby method calling &lt;code class=&quot;highlighter-rouge&quot;&gt;match?&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;TRegex &lt;em&gt;automatically uses SIMD&lt;/em&gt;, and specifically the best SIMD variant available on the running processor. It is able to do so because of the JIT compiler, which queries the CPU for which SIMD variants are available. TRegex does not need to dispatch between different SIMD variants like C would since the JIT compiler knows the best SIMD variant available. So we get to use SIMD instructions, but we don’t have to complicate a Ruby gem to benefit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Specifically for this Regexp TRegex internally uses a &lt;a href=&quot;https://github.com/oracle/graal/blob/9421c159c65b240a5094c1aaabd16cfc9b8897f1/truffle/src/com.oracle.truffle.api.strings/src/com/oracle/truffle/api/strings/TruffleString.java#L3885&quot;&gt;ByteIndexOfCodePointSetNode&lt;/a&gt;, which automatically creates a lookup table for the character range in that regexp (&lt;code class=&quot;highlighter-rouge&quot;&gt;[&quot;\\\x0-\x1f]&lt;/code&gt;) and also automatically uses SIMD.
If you would like to know more about TRegex, the creator of TRegex and I did a &lt;a href=&quot;https://rubykaigi.org/2021-takeout/presentations/eregontp.html&quot;&gt;talk about it at RubyKaigi 2021&lt;/a&gt;.
Kevin Menard also gave a &lt;a href=&quot;https://www.youtube.com/watch?v=XpF3H6w1NSY&quot;&gt;talk about ReDoS&lt;/a&gt; and solutions at RailsConf 2022.&lt;/p&gt;

&lt;p&gt;The JSON String Escaping example above is not an isolated case where TruffleRuby coincidentally is faster.
Having a fast regular expression engine allows for more idiomatic solutions and allows Ruby code running on TruffleRuby to be faster than C code in many cases.
Here are two more real world examples where TruffleRuby uses Regexps and it’s faster than C code!&lt;/p&gt;

&lt;h2 id=&quot;timenewstring&quot;&gt;Time.new(String)&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Time.new&lt;/code&gt; since Ruby 3.2 accepts a String argument.&lt;/p&gt;

&lt;p&gt;CRuby implements it by &lt;a href=&quot;https://github.com/ruby/ruby/blob/1ce10f3fbb037bc8383d0cf7fe72d9f261669246/time.c#L2620-L2700&quot;&gt;parsing strings with &lt;code class=&quot;highlighter-rouge&quot;&gt;char&lt;/code&gt; pointers in about 80 lines of C&lt;/a&gt;.
TruffleRuby implements it in a &lt;a href=&quot;https://github.com/oracle/truffleruby/blob/48a0a6264b2f0823f8b32200677b2fa8c649f681/src/main/ruby/truffleruby/core/truffle/time_operations.rb#L99-L110&quot;&gt;straightforward way with a Regexp&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'benchmark/ips'&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;NOW_STRING&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Benchmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ips&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Time.new(String)&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;NOW_STRING&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ ruby --yjit bench_time_new_string.rb
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT +PRISM [x86_64-linux]
Time.new(String)      1.595M (± 0.5%) i/s  (626.84 ns/i)

$ ruby bench_time_new_string.rb
truffleruby 24.1.1, like ruby 3.2.4, Oracle GraalVM JVM [x86_64-linux]
Time.new(String)      3.923M (± 5.4%) i/s  (254.90 ns/i)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;TruffleRuby is &lt;strong&gt;2.5&lt;/strong&gt; times faster here, while having a much simpler and cleaner implementation.&lt;/p&gt;

&lt;p&gt;Let’s benchmark just the Regexp, although we use &lt;code class=&quot;highlighter-rouge&quot;&gt;match&lt;/code&gt; here since we need the capture groups:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'benchmark/ips'&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;NOW_STRING&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;TIME_REGEXP&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/\A (?&amp;lt;year&amp;gt;\d{4,5})
(?:
  - (?&amp;lt;month&amp;gt;\d{2})
  - (?&amp;lt;mday&amp;gt; \d{2})
  [ T] (?&amp;lt;hour&amp;gt; \d{2})
     : (?&amp;lt;min&amp;gt;  \d{2})
     : (?&amp;lt;sec&amp;gt;  \d{2})
     (?:\. (?&amp;lt;usec&amp;gt; \d+) )?
 (?:\s* (?&amp;lt;offset&amp;gt;\S+))?
)?\z/x&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Benchmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ips&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;TIME_REGEXP.match(NOW_STRING)&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;TIME_REGEXP&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;NOW_STRING&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ ruby --yjit bench_time_new_string_regexp.rb
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT +PRISM [x86_64-linux]
TIME_REGEXP.match(NOW_STRING)      1.234M (± 0.8%) i/s  (810.47 ns/i)

$ ruby bench_time_new_string_regexp.rb
truffleruby 24.1.1, like ruby 3.2.4, Oracle GraalVM JVM [x86_64-linux]
TIME_REGEXP.match(NOW_STRING)     33.448M (± 9.9%) i/s   (29.90 ns/i)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;TruffleRuby is &lt;strong&gt;27&lt;/strong&gt; times faster for this Regexp.
So the &lt;code class=&quot;highlighter-rouge&quot;&gt;Time.new(String)&lt;/code&gt; benchmark is actually spending a lot of time in creating and initializing the &lt;code class=&quot;highlighter-rouge&quot;&gt;Time&lt;/code&gt; object.&lt;/p&gt;

&lt;h2 id=&quot;stringscannerscan_integer&quot;&gt;StringScanner#scan_integer&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;StringScanner#scan_integer&lt;/code&gt; is a method &lt;a href=&quot;https://github.com/ruby/strscan/pull/115&quot;&gt;recently added&lt;/a&gt; to StringScanner.&lt;/p&gt;

&lt;p&gt;CRuby &lt;a href=&quot;https://github.com/ruby/strscan/blob/4aadfc8408c733903f243c42eeaf4c7c893f3a9c/ext/strscan/strscan.c#L1288-L1401&quot;&gt;implements it in 114 lines of C&lt;/a&gt;.
TruffleRuby implements it &lt;a href=&quot;https://github.com/oracle/truffleruby/blob/48a0a6264b2f0823f8b32200677b2fa8c649f681/lib/truffle/strscan.rb#L265-L282&quot;&gt;in 18 lines of Ruby using Regexps&lt;/a&gt;.
We use TruffleRuby master here since the method was introduced after the last TruffleRuby release.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'benchmark/ips'&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'strscan'&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;SCANNER&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;StringScanner&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;123456789&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Benchmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ips&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;StringScanner#scan_integer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;SCANNER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;reset&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;123456789&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;SCANNER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;scan_integer&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ ruby --yjit bench_scan_integer.rb
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT +PRISM [x86_64-linux]
StringScanner#scan_integer     10.530M (± 0.6%) i/s   (94.97 ns/i)

$ ruby bench_scan_integer.rb
truffleruby 25.0.0-dev-48a0a626, like ruby 3.3.5, Oracle GraalVM JVM [x86_64-linux]
StringScanner#scan_integer     30.230M (± 3.0%) i/s   (33.08 ns/i)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;TruffleRuby is about &lt;strong&gt;3&lt;/strong&gt; times faster.
BTW, TruffleRuby implements &lt;code class=&quot;highlighter-rouge&quot;&gt;StringScanner&lt;/code&gt; &lt;a href=&quot;https://github.com/oracle/truffleruby/blob/48a0a6264b2f0823f8b32200677b2fa8c649f681/lib/truffle/strscan.rb&quot;&gt;entirely in Ruby code&lt;/a&gt;.
Ruby code is not only shorter and more expressive, it’s also more correct.
To that point, while implementing new &lt;code class=&quot;highlighter-rouge&quot;&gt;StringScanner&lt;/code&gt; methods we found &lt;a href=&quot;https://github.com/ruby/strscan/issues?q=is%3Aissue%20author%3Aandrykonchin%20OR%20author%3Aeregon%20&quot;&gt;6 issues&lt;/a&gt;
with the C extension implementation of &lt;code class=&quot;highlighter-rouge&quot;&gt;StringScanner&lt;/code&gt;: 2 segfaults and 4 inconsistent/incorrect behaviors.&lt;/p&gt;

&lt;p&gt;Let’s also benchmark just the Regexp for completeness:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'benchmark/ips'&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;INPUT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;123456789&quot;&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Benchmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ips&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\A&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;[+-]?&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\d&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;+/.match(INPUT)&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/\A[+-]?\d+/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;INPUT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ ruby --yjit bench_scan_integer_regexp.rb
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT +PRISM [x86_64-linux]
/A[+-]?d+/.match(INPUT)      1.972M (± 0.8%) i/s  (507.00 ns/i)

$ ruby bench_scan_integer_regexp.rb
truffleruby 25.0.0-dev-48a0a626, like ruby 3.3.5, Oracle GraalVM JVM [x86_64-linux]
/A[+-]?d+/.match(INPUT)     61.086M (± 6.9%) i/s   (16.37 ns/i)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;TruffleRuby is &lt;strong&gt;31&lt;/strong&gt; times faster for this Regexp.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Sometimes, such as in the cases shown in this blog post, the idiomatic pure-Ruby solution is also the fastest and even faster than C code or inline assembly.
This is not new on TruffleRuby, pure-Ruby code is often faster than everything else, including C extensions on CRuby.
Ruby code is more expressive and higher-level which makes some optimizations possible that otherwise wouldn’t be if written in a lower-level language.&lt;/p&gt;

&lt;p&gt;If you want to run idiomatic Ruby code fast, give &lt;a href=&quot;https://github.com/oracle/truffleruby?tab=readme-ov-file#readme&quot;&gt;TruffleRuby&lt;/a&gt; a try.&lt;/p&gt;</content><author><name>Benoit Daloze</name></author><summary type="html"></summary></entry><entry><title type="html">Benchmarking Ruby Parsers</title><link href="https://eregon.me/blog/2024/10/27/benchmarking-ruby-parsers.html" rel="alternate" type="text/html" title="Benchmarking Ruby Parsers" /><published>2024-10-27T00:00:00+02:00</published><updated>2024-10-27T00:00:00+02:00</updated><id>https://eregon.me/blog/2024/10/27/benchmarking-ruby-parsers</id><content type="html" xml:base="https://eregon.me/blog/2024/10/27/benchmarking-ruby-parsers.html">&lt;p&gt;The new &lt;a href=&quot;https://github.com/ruby/prism&quot;&gt;Prism&lt;/a&gt; parser has &lt;a href=&quot;https://bugs.ruby-lang.org/issues/20564&quot;&gt;become the default&lt;/a&gt; in Ruby 3.4.0 preview 2.&lt;/p&gt;

&lt;p&gt;Let’s benchmark Ruby parsers and find out how fast they are.&lt;/p&gt;

&lt;p&gt;We run benchmarks on Ruby 3.4.0 preview 2 with YJIT (&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby 3.4.0preview2 (2024-10-07 master 32c733f57b) +YJIT +PRISM [x86_64-linux]&lt;/code&gt;), on an &lt;code class=&quot;highlighter-rouge&quot;&gt;AMD Ryzen 7 3700X 8-Core Processor&lt;/code&gt; and a NVMe M.2 SSD, on Linux, with frequency scaling disabled and the &lt;code class=&quot;highlighter-rouge&quot;&gt;performance&lt;/code&gt; CPU governor.&lt;/p&gt;

&lt;p&gt;We will compare:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;prism 1.2.0&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;parser 3.3.5.0&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby_parser 3.21.1&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Ripper&lt;/code&gt; from &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby 3.4.0preview2&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree&lt;/code&gt; from &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby 3.4.0preview2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which are the latest releases at the time of writing.&lt;/p&gt;

&lt;p&gt;Our corpus will be all &lt;code class=&quot;highlighter-rouge&quot;&gt;.rb&lt;/code&gt; files in &lt;code class=&quot;highlighter-rouge&quot;&gt;railties&lt;/code&gt; &lt;code class=&quot;highlighter-rouge&quot;&gt;7.2.1.2&lt;/code&gt;, that is 151 files, consisting of 14625 lines and 455673 bytes in total.&lt;/p&gt;

&lt;p&gt;Let me start with: benchmarking parsers fairly is &lt;a href=&quot;https://github.com/ruby/prism/issues/1692#issuecomment-1764595938&quot;&gt;hard&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;All these parsers return different ASTs.
Some have more information than others and might be more or less practical to use, but they are all ASTs representing Ruby code.&lt;/p&gt;

&lt;p&gt;One major concern performance-wise is that some parsers might allocate Ruby objects lazily and instead only keep some C structs around.
This is the case of &lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree&lt;/code&gt;, as can be seen by looking at allocations:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; 36835 Prism::Node allocations for Prism.parse
 37762 Parser::AST::Node allocations for Parser gem
 79749 Sexp allocations for RubyParser
230854 Array allocations for Ripper.sexp
   151 RubyVM::AbstractSyntaxTree::Node allocations for RubyVM::AbstractSyntaxTree
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;36835 nodes for Prism is consistent with the 37762 nodes for the Parser gem.
79749 for RubyParser seems a bit much but still the same order of magnitude.
230854 for Ripper looks to be an outlier, but one inaccuracy here is Ripper uses Arrays not only for nodes but also for positions like &lt;code class=&quot;highlighter-rouge&quot;&gt;[line, column]&lt;/code&gt; and Arrays might be used for other things too.
&lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree&lt;/code&gt; only allocates 151 nodes (one per file), which proves it creates Ruby node objects lazily.&lt;/p&gt;

&lt;p&gt;One way to avoid that laziness is to &lt;a href=&quot;https://gist.github.com/eregon/27b56c686e238bafa6b46b73f17a4618#file-bench_parsers-rb-L15-L48&quot;&gt;walk all nodes in the AST&lt;/a&gt;.
Doing that, we get:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; 36835 Prism::Node allocations for Prism.parse
 36835 Prism::Node allocations for Prism.parse + walk
 37762 Parser::AST::Node allocations for Parser gem
 37762 Parser::AST::Node allocations for Parser gem + walk
 79749 Sexp allocations for RubyParser
 79749 Sexp allocations for RubyParser + walk
230854 Array allocations for Ripper.sexp
230854 Array allocations for Ripper.sexp + walk
   151 RubyVM::AbstractSyntaxTree::Node allocations for RubyVM::AbstractSyntaxTree
 36262 RubyVM::AbstractSyntaxTree::Node allocations for RubyVM::AbstractSyntaxTree + walk
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So &lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree&lt;/code&gt; needs special care to be benchmarked fairly.&lt;/p&gt;

&lt;p&gt;While we are at it, here are the total number of Ruby-level allocations:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  75610 total allocations for Prism.parse
 112445 total allocations for Prism.parse + walk
  diff: +36835 Arrays

 823320 total allocations for Parser gem
 823320 total allocations for Parser gem + walk

1028578 total allocations for RubyParser
1028578 total allocations for RubyParser + walk

 341687 total allocations for Ripper.sexp
 341687 total allocations for Ripper.sexp + walk

  16044 total allocations for RubyVM::AbstractSyntaxTree
  92495 total allocations for RubyVM::AbstractSyntaxTree + walk
  diff: +38568 Arrays, +36111 RubyVM::AbstractSyntaxTree::Node, +1689 Strings, +83 Regexps
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can see walking the AST:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;causes extra Array allocations for Prism because each call to &lt;code class=&quot;highlighter-rouge&quot;&gt;child_nodes&lt;/code&gt; allocates an Array.&lt;/li&gt;
  &lt;li&gt;does not allocate for the Parser gem because it keeps child nodes in an Array internally and &lt;code class=&quot;highlighter-rouge&quot;&gt;#children&lt;/code&gt; just returns that.&lt;/li&gt;
  &lt;li&gt;does not allocate for RubyParser and Ripper because their representation is just Arrays (or &lt;code class=&quot;highlighter-rouge&quot;&gt;Sexp&lt;/code&gt;, a subclass of Array).&lt;/li&gt;
  &lt;li&gt;allocates Arrays and nodes for &lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;parsing-and-walking&quot;&gt;Parsing and Walking&lt;/h2&gt;

&lt;p&gt;So to try to benchmark &lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree&lt;/code&gt; fairly we can walk the AST for all parsers:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Calculating -------------------------------------
               Prism.parse + walk     27.087 (± 0.0%) i/s   (36.92 ms/i) -    136.000 in   5.021906s
                Parser gem + walk      2.246 (± 0.0%) i/s  (445.33 ms/i) -     12.000 in   5.343971s
                RubyParser + walk      1.632 (± 0.0%) i/s  (612.90 ms/i) -      9.000 in   5.516460s
               Ripper.sexp + walk     11.265 (± 0.0%) i/s   (88.77 ms/i) -     57.000 in   5.065812s
RubyVM::AbstractSyntaxTree + walk     24.272 (± 4.1%) i/s   (41.20 ms/i) -    122.000 in   5.031333s

Comparison:
               Prism.parse + walk:       27.1 i/s
RubyVM::AbstractSyntaxTree + walk:       24.3 i/s - 1.12x  slower
               Ripper.sexp + walk:       11.3 i/s - 2.40x  slower
                Parser gem + walk:        2.2 i/s - 12.06x  slower
                RubyParser + walk:        1.6 i/s - 16.60x  slower
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;However, this gives an advantage to Ripper, the Parser gem, and RubyParser as they don’t need extra Array allocations to walk the AST.
Despite that advantage, all three are slower than Prism.&lt;/p&gt;

&lt;p&gt;Prism and &lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree&lt;/code&gt; are more than 10 times as fast as the Parser gem and RubyParser in this benchmark.
That’s quite the speedup.&lt;/p&gt;

&lt;h2 id=&quot;parsing-and-not-walking&quot;&gt;Parsing and not Walking&lt;/h2&gt;

&lt;p&gt;Or we can exclude &lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree&lt;/code&gt; and not walk the AST:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Calculating -------------------------------------
         Prism.parse     32.590 (± 0.0%) i/s   (30.68 ms/i) -    165.000 in   5.063030s
          Parser gem      2.256 (± 0.0%) i/s  (443.30 ms/i) -     12.000 in   5.319708s
          RubyParser      1.638 (± 0.0%) i/s  (610.58 ms/i) -      9.000 in   5.495618s
         Ripper.sexp     11.834 (± 0.0%) i/s   (84.51 ms/i) -     60.000 in   5.077234s

Comparison:
Prism.parse:       32.6 i/s
Ripper.sexp:       11.8 i/s - 2.75x  slower
 Parser gem:        2.3 i/s - 14.45x  slower
 RubyParser:        1.6 i/s - 19.90x  slower
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;just-parsing&quot;&gt;Just Parsing&lt;/h2&gt;

&lt;p&gt;We can also find a way to measure just the parsing itself and the creation of C data structures to hold the parse result.
In this configuration the parser will allocate as few Ruby objects as possible.
This approach is feasible for &lt;code class=&quot;highlighter-rouge&quot;&gt;Prism&lt;/code&gt; and for &lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree&lt;/code&gt;.
&lt;code class=&quot;highlighter-rouge&quot;&gt;Prism.profile&lt;/code&gt; parses to C structs and does nothing else.
&lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree.parse&lt;/code&gt; uses &lt;a href=&quot;https://github.com/ruby/ruby/blob/v3_4_0_preview2/parse.y&quot;&gt;parse.y&lt;/a&gt;, the LALR-based parser used in CRuby 3.3 and before.
&lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree.parse&lt;/code&gt; does very little extra work besides parsing to C structs, which I double-checked by measuring the time around &lt;code class=&quot;highlighter-rouge&quot;&gt;yyparse()&lt;/code&gt; (the entry point for &lt;code class=&quot;highlighter-rouge&quot;&gt;parse.y&lt;/code&gt;) for a large file.
About 99.8% of the time is spent inside &lt;code class=&quot;highlighter-rouge&quot;&gt;yyparse()&lt;/code&gt;, so &lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree.parse&lt;/code&gt; is a good approximation for &lt;code class=&quot;highlighter-rouge&quot;&gt;yyparse()&lt;/code&gt;/&lt;code class=&quot;highlighter-rouge&quot;&gt;parse.y&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;151 files, 14625 lines, 455673 bytes

0 total allocations for Prism.profile

16095 total allocations for RubyVM::AbstractSyntaxTree:
  15559 Strings, 302 Datas, 151 RubyVM::AbstractSyntaxTree::Node, 83 Regexps

Calculating -------------------------------------
             Prism.profile    100.447 (± 2.0%) i/s    (9.96 ms/i) -    510.000 in   5.079433s
RubyVM::AbstractSyntaxTree     39.186 (± 2.6%) i/s   (25.52 ms/i) -    198.000 in   5.056720s

Comparison:
             Prism.profile:      100.4 i/s
RubyVM::AbstractSyntaxTree:       39.2 i/s - 2.56x  slower
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Prism parses this corpus of 455673 bytes in 9.96 ms, which is 43.6 MB/s, compared to 17.0 MB/s for &lt;code class=&quot;highlighter-rouge&quot;&gt;parse.y&lt;/code&gt;.
In other words, &lt;code class=&quot;highlighter-rouge&quot;&gt;Prism&lt;/code&gt; is 2.56 times as fast as &lt;code class=&quot;highlighter-rouge&quot;&gt;parse.y&lt;/code&gt; in parsing this corpus of Ruby code!&lt;/p&gt;

&lt;p&gt;There are 16095 allocations for &lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree&lt;/code&gt;, most of which are Strings.
Of those Strings, most are the one String per line (14625 lines) that the lexer used by &lt;code class=&quot;highlighter-rouge&quot;&gt;yyparse()&lt;/code&gt; allocates (in &lt;code class=&quot;highlighter-rouge&quot;&gt;rb_parser_lex_get_str()&lt;/code&gt;).
We see 151 &lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree::Node&lt;/code&gt;: one per file, as expected.&lt;/p&gt;

&lt;h2 id=&quot;from-source-to-cruby-bytecode&quot;&gt;From Source To CRuby Bytecode&lt;/h2&gt;

&lt;p&gt;We have one more benchmark to measure compilation from source code to CRuby bytecode using &lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::InstructionSequence.compile_prism&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::InstructionSequence.compile_parsey&lt;/code&gt; (for &lt;code class=&quot;highlighter-rouge&quot;&gt;parse.y&lt;/code&gt;).
In this benchmark the input and output are exactly the same for both variants so it is as fair as it gets.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;151 files, 14625 lines, 455673 bytes

10477 total allocations for RubyVM::InstructionSequence.compile_prism:
  3753 Strings, 3389 Arrays, 2717 Imemos, 378 Hashs, 151 RubyVM::InstructionSequence, 83 Regexps, 6 Structs

28782 total allocations for RubyVM::InstructionSequence.compile_parsey, including 151 RubyVM::InstructionSequence:
  18476 Strings, 4237 Arrays, 2868 Imemos, 2501 Datas, 377 Hashs, 166 Regexps, 151 RubyVM::InstructionSequence, 6 Structs

Calculating -------------------------------------
 RubyVM::InstructionSequence.compile_prism     36.499 (± 0.0%) i/s   (27.40 ms/i) -    183.000 in   5.014395s
RubyVM::InstructionSequence.compile_parsey     25.050 (± 0.0%) i/s   (39.92 ms/i) -    126.000 in   5.030555s

Comparison:
 RubyVM::InstructionSequence.compile_prism:       36.5 i/s
RubyVM::InstructionSequence.compile_parsey:       25.1 i/s - 1.46x  slower
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s a nice 1.46x speedup for Prism here.
This improvement makes Ruby programs and applications boot faster on CRuby, by reducing the amount of time spent parsing and compiling to bytecode.
Prism is also used in &lt;a href=&quot;https://github.com/natalie-lang/natalie/pull/1213&quot;&gt;other&lt;/a&gt; &lt;a href=&quot;https://github.com/oracle/truffleruby/issues/3117&quot;&gt;Ruby&lt;/a&gt; &lt;a href=&quot;https://blog.jruby.org/2024/02/jruby-prism-parser&quot;&gt;implementations&lt;/a&gt; and has sped up parsing and boot times there as well.&lt;/p&gt;

&lt;h2 id=&quot;script&quot;&gt;Script&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://gist.github.com/eregon/27b56c686e238bafa6b46b73f17a4618&quot;&gt;Here is the script&lt;/a&gt; I used to benchmark and get allocation counts.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Benchmarking Ruby parsers is trickier than one might expect, but overall the result is clear: Prism is the fastest Ruby parser.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;When parsing and walking, Prism is 12% faster than &lt;code class=&quot;highlighter-rouge&quot;&gt;RubyVM::AbstractSyntaxTree&lt;/code&gt; and 12x as fast as the Parser gem!&lt;/li&gt;
  &lt;li&gt;When parsing to C structs, Prism is 2.56x as fast as &lt;code class=&quot;highlighter-rouge&quot;&gt;parse.y&lt;/code&gt;. Who thought parsing Ruby in C could be made more than twice as fast?&lt;/li&gt;
  &lt;li&gt;When compiling to bytecode, Prism is 1.46x as fast as &lt;code class=&quot;highlighter-rouge&quot;&gt;parse.y&lt;/code&gt;, speeding up boot times.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beyond raw performance, Prism also provides a great and &lt;a href=&quot;https://ruby.github.io/prism/&quot;&gt;well documented&lt;/a&gt; API, which makes it much easier to build fast tooling for and in Ruby.&lt;/p&gt;</content><author><name>Benoit Daloze</name></author><summary type="html">The new Prism parser has become the default in Ruby 3.4.0 preview 2.</summary></entry><entry><title type="html">Benchmarking CRuby, MJIT, YJIT, JRuby and TruffleRuby</title><link href="https://eregon.me/blog/2022/01/06/benchmarking-cruby-mjit-yjit-jruby-truffleruby.html" rel="alternate" type="text/html" title="Benchmarking CRuby, MJIT, YJIT, JRuby and TruffleRuby" /><published>2022-01-06T00:00:00+01:00</published><updated>2022-01-06T00:00:00+01:00</updated><id>https://eregon.me/blog/2022/01/06/benchmarking-cruby-mjit-yjit-jruby-truffleruby</id><content type="html" xml:base="https://eregon.me/blog/2022/01/06/benchmarking-cruby-mjit-yjit-jruby-truffleruby.html">&lt;p&gt;In this blog post we benchmark many Ruby versions and the latest Ruby Just-in-Time compilers (JITs)
on the newest Ruby benchmark suite,  &lt;a href=&quot;https://github.com/Shopify/yjit-bench&quot;&gt;yjit-bench&lt;/a&gt;.
As a teaser, the geometric mean speedups compared to CRuby 3.1 on these 14 benchmarks are: MJIT 1.26x, YJIT 1.39x, JRuby 1.86x and TruffleRuby 6.23x.
Read on to find more about the benchmarks and gain insights on these speedups.
This blog post is also available &lt;a href=&quot;https://medium.com/graalvm/benchmarking-cruby-mjit-yjit-jruby-and-truffleruby-6a7178ca6906&quot;&gt;on Medium&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/banner.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;outline&quot;&gt;Outline&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#approach&quot;&gt;Approach&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#benchmarks&quot;&gt;Benchmarks&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#summary&quot;&gt;Summary&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;approach&quot;&gt;Approach&lt;/h2&gt;

&lt;h3 id=&quot;rubies&quot;&gt;Rubies&lt;/h3&gt;

&lt;p&gt;We benchmark the following Ruby versions:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;CRuby 2.0: &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby 2.0.0p648 (2015-12-16 revision 53162) [x86_64-linux]&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;CRuby 2.7: &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby 2.7.5p203 (2021-11-24 revision f69aeb8314) [x86_64-linux]&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;CRuby 3.0: &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby 3.0.3p157 (2021-11-24 revision 3fb7d2cadc) [x86_64-linux]&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;CRuby 3.1: &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [x86_64-linux]&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;CRuby 3.1+MJIT: &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) +MJIT [x86_64-linux]&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;CRuby 3.1+YJIT: &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) +YJIT [x86_64-linux]&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;JRuby+invokedynamic, on the system JDK 11: &lt;code class=&quot;highlighter-rouge&quot;&gt;jruby 9.3.2.0 (2.6.8) 2021-12-01 0b8223f905 OpenJDK 64-Bit Server VM 11.0.10+9 on 11.0.10+9 +indy +jit [linux-x86_64]&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;TruffleRuby JVM EE (JDK 11): &lt;code class=&quot;highlighter-rouge&quot;&gt;truffleruby 22.0.0-dev-9bdb2dfc, like ruby 3.0.2, GraalVM EE JVM [x86_64-linux]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We ran all the benchmarks on 4 January 2022 on an &lt;code class=&quot;highlighter-rouge&quot;&gt;AMD Ryzen 7 3700X 8-Core Processor&lt;/code&gt;, with 32GB of memory and a NVMe M.2 SSD, on Linux.&lt;/p&gt;

&lt;p&gt;We chose CRuby 3.1 as the baseline (= 1.0x) in order to easily compare how much MJIT and YJIT gain over CRuby without JIT (the default).&lt;/p&gt;

&lt;p&gt;CRuby 2.0 can only run a small subset of the benchmarks.
CRuby 3.1 includes both MJIT and YJIT which makes it convenient to measure them.
For TruffleRuby we picked a single variant (out of Native/JVM CE/EE) to keep the charts readable as there are already many Ruby implementations.
We used TruffleRuby from the release branch of the upcoming GraalVM 22.0.&lt;/p&gt;

&lt;h3 id=&quot;yjit-bench&quot;&gt;yjit-bench&lt;/h3&gt;

&lt;p&gt;We run all 14 macro benchmarks from the &lt;a href=&quot;https://github.com/Shopify/yjit-bench&quot;&gt;yjit-bench&lt;/a&gt; benchmark suite at commit &lt;a href=&quot;https://github.com/Shopify/yjit-bench/tree/1751916ceb&quot;&gt;1751916ceb&lt;/a&gt;, except for the &lt;code class=&quot;highlighter-rouge&quot;&gt;jekyll&lt;/code&gt; benchmark because it is very noisy and does not work on several Ruby versions.
We do not report the micro benchmarks (&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/getivar.rb&quot;&gt;example&lt;/a&gt;), since those seem to be there mostly to tweak JITs rather than to compare general Ruby performance. Of course micro benchmarks do not represent anything close to a real workload.
As a fun fact, TruffleRuby optimizes away (&amp;gt;1000x faster) half of those micro benchmarks.&lt;/p&gt;

&lt;p&gt;The yjit-bench benchmark suite contains a variety of benchmarks, and is maintained principally by Noah Gibbs (who also worked on benchmarks for Ruby 3x3) and Maxime Chevalier-Boisvert (leading YJIT) at Shopify.
One significant advantage of this benchmark suite is all benchmarks are really easy to run, any setup (including &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle install&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;db:migrate&lt;/code&gt;) is done automatically and running a benchmark is just &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby -I./harness benchmarks/some/benchmark.rb&lt;/code&gt;.
There are no subprocesses, no magic, and so it is really easy to run them and profile them.&lt;/p&gt;

&lt;h3 id=&quot;warmup&quot;&gt;Warmup&lt;/h3&gt;

&lt;p&gt;We measure peak performance, i.e., performance after enough warmup, that is performance after the relevant methods are compiled.
This is what is typically used to compare JITs, and it is also what this benchmark suite aims to measure.
All benchmarks were run with enough warmup.
The warmup is automatically detected by using a &lt;a href=&quot;https://github.com/eregon/yjit-bench/blob/harness-warmup-20220104/harness-warmup/harness.rb&quot;&gt;custom harness&lt;/a&gt; which tracks the median absolute deviation over all iterations so far and keeps running more iterations until the median absolute deviation threshold is reached (more details &lt;a href=&quot;https://github.com/Shopify/yjit-metrics/issues/30#issuecomment-955207783&quot;&gt;here&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In general, more advanced JITs need more warmup.
The TruffleRuby and GraalVM teams are working on this issue by &lt;a href=&quot;https://medium.com/graalvm/multi-tier-compilation-in-graalvm-5fbc65f92402&quot;&gt;using multi-tier compilation&lt;/a&gt;, &lt;a href=&quot;https://www.graalvm.org/graalvm-as-a-platform/language-implementation-framework/AOTOverview/&quot;&gt;persisting the JITed code&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/smarr/status/1383076749043511300&quot;&gt;optimizing interpreter performance&lt;/a&gt;, among other ideas.&lt;/p&gt;

&lt;p&gt;The plots report the &lt;a href=&quot;https://en.wikipedia.org/wiki/Median&quot;&gt;median&lt;/a&gt; as the bar heights and the &lt;a href=&quot;https://en.wikipedia.org/wiki/Median_absolute_deviation&quot;&gt;median absolute deviation&lt;/a&gt; as error bars, for the second half of all iterations run. The first half is considered warmup and typically more noisy.
Those estimators are chosen because they are robust (which means they are not affected by outliers like a GC in one iteration), unlike the usual mean and standard deviation.
As one can see, the error bars are very small for almost all benchmarks, which is a further indication of enough warmup and having reached stable performance.&lt;/p&gt;

&lt;p&gt;The median is the middle value when we sort all values.
The median absolute deviation is the median of all absolute deviations, which are the absolute value of the difference between a given value and the median.
In other words, the median represents the middle value (typically the closest value to most values), and the median absolute deviation shows us how far other values are from the median.&lt;/p&gt;

&lt;h2 id=&quot;benchmarks&quot;&gt;Benchmarks&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/activerecord.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/activerecord/benchmark.rb&quot;&gt;The activerecord benchmark&lt;/a&gt; uses Rails’ ActiveRecord to query the database and return the title of the first &lt;code class=&quot;highlighter-rouge&quot;&gt;Post&lt;/code&gt;.
The &lt;code class=&quot;highlighter-rouge&quot;&gt;sqlite3&lt;/code&gt; in-memory database is used for convenience (&lt;code class=&quot;highlighter-rouge&quot;&gt;sqlite3&lt;/code&gt; is a C extension gem).
On JRuby, &lt;code class=&quot;highlighter-rouge&quot;&gt;activerecord-jdbcsqlite3-adapter&lt;/code&gt; is used instead.&lt;/p&gt;

&lt;p&gt;One might think this benchmark is dominated by database performance but it seems clearly not the case, with YJIT being 1.42x the speed of CRuby 3.1 and TruffleRuby showing a 4.54x speedup.
There is quite a bit of Ruby logic in ActiveRecord and some in the &lt;code class=&quot;highlighter-rouge&quot;&gt;sqlite3&lt;/code&gt; gem, which Ruby JITs can potentially optimize.
TruffleRuby might also run the C extension faster since TruffleRuby JIT-compiles C extensions together with Ruby code.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/binarytrees.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/binarytrees/benchmark.rb&quot;&gt;The binarytrees benchmark&lt;/a&gt; (from the &lt;a href=&quot;https://benchmarksgame-team.pages.debian.net/benchmarksgame/description/binarytrees.html#binarytrees&quot;&gt;Computer Language Benchmarks Game&lt;/a&gt;, abbreviated CLBG later) creates binary trees using lots of recursive calls.
It also stresses the GC due to many allocation and keeps one tree alive for a while.
MJIT is doing well here with a 1.79x speedup, JRuby 2.06x and TruffleRuby 2.73x.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/erubi.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/erubi/benchmark.rb&quot;&gt;The erubi benchmark&lt;/a&gt; renders &lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/erubi/simple_template.erb&quot;&gt;an ERB template&lt;/a&gt; (&lt;code class=&quot;highlighter-rouge&quot;&gt;gem server&lt;/code&gt;’s index with 322 gems) using the &lt;a href=&quot;https://github.com/jeremyevans/erubi&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;erubi&lt;/code&gt; gem&lt;/a&gt;, which is the default template engine in Rails.
The rendered template has a length of 166563 bytes.
TruffleRuby performs great here due to &lt;a href=&quot;https://eregon.me/blog/assets/research/specializing-ropes-for-ruby.pdf&quot;&gt;its very efficient String concatenation&lt;/a&gt; and achieves a 5.26x speedup.
YJIT is currently not optimizing this benchmark well due to &lt;a href=&quot;https://github.com/Shopify/yjit/issues/297&quot;&gt;not supporting a bytecode instruction&lt;/a&gt; used in this benchmark.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/erubi_rails.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/erubi_rails/benchmark.rb&quot;&gt;The erubi_rails benchmark&lt;/a&gt; is a small Rails application which renders &lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/erubi_rails/app/views/fake_discourse/topics_show.html.erb&quot;&gt;an ERB view&lt;/a&gt; coming from &lt;a href=&quot;https://www.discourse.org/&quot;&gt;Discourse&lt;/a&gt;.
The rendered template has a length of 9369 bytes.
The benchmark renders a topic’s page 100 times per iteration, in process (no network involved).
No database is involved in this benchmark as it focuses on &lt;code class=&quot;highlighter-rouge&quot;&gt;erubi&lt;/code&gt; in the context of Rails.
YJIT gets a speedup of 1.31x, JRuby of 1.82x and TruffleRuby of 3.3x.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/fannkuchredux.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/fannkuchredux/benchmark.rb&quot;&gt;The fannkuchredux benchmark&lt;/a&gt; (&lt;a href=&quot;https://benchmarksgame-team.pages.debian.net/benchmarksgame/description/fannkuchredux.html#fannkuchredux&quot;&gt;from CLBG&lt;/a&gt;) creates and manipulates &lt;code class=&quot;highlighter-rouge&quot;&gt;Array&lt;/code&gt;s extensively.
MJIT and YJIT seem to not achieve any speedup here, probably because the benchmark is heavy on &lt;code class=&quot;highlighter-rouge&quot;&gt;Array&lt;/code&gt; operations and Array operations are defined in C in CRuby and so MJIT/YJIT cannot optimize them (except for simple &lt;code class=&quot;highlighter-rouge&quot;&gt;a[i]&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;a[i] = v&lt;/code&gt; but they do not seem to optimize it better than the interpreter in this benchmark).
JRuby and TruffleRuby OTOH can optimize and JIT compile Array operations, and &lt;a href=&quot;https://speakerdeck.com/eregon/parallel-and-thread-safe-ruby-at-high-speed-with-truffleruby?slide=62&quot;&gt;TruffleRuby uses storage strategies&lt;/a&gt; for Arrays.
JRuby gets a speedup of 5.53x, TruffleRuby gets a speedup of 33.88x.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/hexapdf.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/hexapdf/benchmark.rb&quot;&gt;The hexapdf benchmark&lt;/a&gt; uses the &lt;code class=&quot;highlighter-rouge&quot;&gt;hexapdf&lt;/code&gt; gem, wraps a long text (&lt;a href=&quot;https://gutenberg.org/ebooks/1727&quot;&gt;The Odyssey&lt;/a&gt;) to a line width of 50 characters and renders it to a PDF file, saved to &lt;code class=&quot;highlighter-rouge&quot;&gt;/tmp&lt;/code&gt;. &lt;code class=&quot;highlighter-rouge&quot;&gt;hexapdf&lt;/code&gt; is written entirely in Ruby (no C extension).
Wrapping text is heavy on arithmetic operations and JITs often do a good job on those.
YJIT gets a speedup of 1.4x, TruffleRuby of 4.38x (with some variability).
CRuby 3.1 MJIT &lt;a href=&quot;https://bugs.ruby-lang.org/issues/18277&quot;&gt;fails this benchmark&lt;/a&gt; with &lt;code class=&quot;highlighter-rouge&quot;&gt;buffer error (Zlib::BufError)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/lee.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/lee/benchmark.rb&quot;&gt;The lee benchmark&lt;/a&gt; is from Chris Seaton (see his &lt;a href=&quot;https://chrisseaton.com/truffleruby/ruby-stm/&quot;&gt;blog post&lt;/a&gt;) and some of his work during his PhD.
The benchmark lays out the wires on a circuit board in a manner which avoids crossing wires.
This is the sequential version of the benchmark.
It seems worth noting the approach itself is from scientific literature and Chris wrote this in straightforward pure Ruby, not particularly optimizing it for any Ruby implementation.
In his blog post he found that TruffleRuby was over 10x faster than CRuby 2.7.
On latest TruffleRuby, we see a speedup of 22x!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/liquid-render.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/liquid-render/benchmark.rb&quot;&gt;The liquid-render benchmark&lt;/a&gt; renders all the &lt;a href=&quot;https://github.com/Shopify/yjit-bench/tree/1751916ceb/benchmarks/liquid-render/performance/tests&quot;&gt;performance test Liquid templates&lt;/a&gt; which are parsed before starting the benchmark.
As you may know, rendering Liquid templates quickly &lt;a href=&quot;https://www.shopify.com/partners/blog/shopify-liquid-debug&quot;&gt;is important for Shopify&lt;/a&gt;.
Liquid is not a typical template engine, it does not generate Ruby code and &lt;code class=&quot;highlighter-rouge&quot;&gt;eval&lt;/code&gt; it and then call the generated method (like ERB engines).
Instead, Liquid &lt;a href=&quot;https://github.com/Shopify/liquid/blob/db3999a008/lib/liquid/tags/if.rb&quot;&gt;interprets&lt;/a&gt; an AST representing the template for &lt;a href=&quot;https://github.com/Shopify/liquid/blob/db3999a008/README.md#introduction&quot;&gt;security reasons and avoiding eval&lt;/a&gt;.
YJIT gets a speedup of 1.61x and TruffleRuby gets a speedup of 4.86x, some of it due to TruffleRuby’s advanced String representation (&lt;a href=&quot;https://eregon.me/blog/assets/research/specializing-ropes-for-ruby.pdf&quot;&gt;Ropes&lt;/a&gt;) and the rest due to JIT compiling the Liquid interpreter better.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/mail.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/mail/benchmark.rb&quot;&gt;The mail benchmark&lt;/a&gt; parses &lt;a href=&quot;https://github.com/jhawthorn/yjit-bench/blob/1751916ceb/benchmarks/mail/raw_email2.eml&quot;&gt;an email&lt;/a&gt; with the &lt;code class=&quot;highlighter-rouge&quot;&gt;mail&lt;/code&gt; gem (&lt;a href=&quot;https://github.com/Shopify/yjit-bench/pull/40&quot;&gt;more context&lt;/a&gt;). Some of the parsing is done by parsers generated by &lt;a href=&quot;http://www.colm.net/open-source/ragel/&quot;&gt;Ragel&lt;/a&gt;.
YJIT and TruffleRuby show nice speedups.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/nbody.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/nbody/benchmark.rb&quot;&gt;The nbody benchmark&lt;/a&gt; (&lt;a href=&quot;https://benchmarksgame-team.pages.debian.net/benchmarksgame/description/nbody.html#nbody&quot;&gt;from CLBG&lt;/a&gt;) models the orbits of Jupiter, Saturn, Uranus and Neptune around the Sun.
The benchmark consists mainly of arithmetic computations and accessing instance variables heavily (planets are represented as objects).
JRuby reaches a good speedup of 4.43x.
TruffleRuby is about 50x the speed of CRuby 3.1, which is simply incredible.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/optcarrot.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/optcarrot/benchmark.rb&quot;&gt;The optcarrot benchmark&lt;/a&gt; is one of the famous benchmarks for Ruby 3x3 (by &lt;a href=&quot;https://github.com/mame/optcarrot&quot;&gt;@mame&lt;/a&gt;, &lt;a href=&quot;https://eregon.me/blog/2016/11/28/optcarrot.html&quot;&gt;my blog post about it&lt;/a&gt;). It is a NES emulator and this benchmark renders 200 frames of the &lt;code class=&quot;highlighter-rouge&quot;&gt;Lan Master&lt;/code&gt; game per iteration.
MJIT is known to be &lt;a href=&quot;https://www.ruby-lang.org/en/news/2020/12/25/ruby-3-0-0-released/&quot;&gt;about 3x faster&lt;/a&gt; than CRuby 2.0 on this benchmark.
In this run, 3.1 MJIT is exactly 3.0x as fast as &lt;strong&gt;CRuby 2.0&lt;/strong&gt;.
TruffleRuby is known to be &lt;a href=&quot;https://eregon.me/blog/2016/11/28/optcarrot.html&quot;&gt;good at it&lt;/a&gt; since the benchmark came out, and it reaches a 10.4x speedup over CRuby 2.0.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/psych-load.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/psych-load/benchmark.rb&quot;&gt;The psych-load benchmark&lt;/a&gt; parses &lt;a href=&quot;https://github.com/Shopify/yjit-bench/tree/1751916ceb/benchmarks/psych-load/yaml&quot;&gt;a set of YAML files&lt;/a&gt; using &lt;code class=&quot;highlighter-rouge&quot;&gt;Psych.load&lt;/code&gt; (same as &lt;code class=&quot;highlighter-rouge&quot;&gt;YAML.load&lt;/code&gt;).
Psych 2.2.4 is used on CRuby 2.0 instead of Psych 4.0.1, since it is the latest Psych version compatible with 2.0.
Although a significant part of the benchmark is likely spent inside the &lt;code class=&quot;highlighter-rouge&quot;&gt;psych&lt;/code&gt; C extension (which uses the &lt;code class=&quot;highlighter-rouge&quot;&gt;libyaml&lt;/code&gt; YAML parser), we see the time is not only spent in C extensions (where CRuby JITs cannot help) but also in Ruby code as MJIT and YJIT show some speedups here.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/railsbench.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/railsbench/benchmark.rb&quot;&gt;The railsbench benchmark&lt;/a&gt; is inspired from k0kubun’s &lt;a href=&quot;https://github.com/k0kubun/railsbench&quot;&gt;railsbench&lt;/a&gt;, which is based on headius’s &lt;a href=&quot;https://github.com/headius/pgrailsbench&quot;&gt;pgrailsbench&lt;/a&gt;. It is a small blog-style Rails app &lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/railsbench/README.md&quot;&gt;scaffolded with a &lt;code class=&quot;highlighter-rouge&quot;&gt;Post&lt;/code&gt; model&lt;/a&gt;. This variant uses &lt;code class=&quot;highlighter-rouge&quot;&gt;sqlite3&lt;/code&gt; as the database for convenience (stored on disk), and runs everything in a single process (much simpler to run).
The benchmark repeatedly visits the index of the posts (both as HTML and JSON) and each of the 100 posts in a &lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/railsbench/benchmark.rb#L16-L21&quot;&gt;pseudorandom order with a fixed seed&lt;/a&gt;. It uses the default ERB templates generated by Rails.
MJIT gets a speedup of 1.09x (&lt;a href=&quot;https://k0kubun.medium.com/ruby-3-jit-can-make-rails-faster-756310f235a&quot;&gt;it took a while for MJIT to speedup Rails&lt;/a&gt;), YJIT of 1.33x, JRuby of 1.13x and TruffleRuby of 3.27x.
In other words, on this small Rails benchmark, TruffleRuby response times are more than 3 times faster than CRuby 3.1!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/rubykon.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/Shopify/yjit-bench/blob/1751916ceb/benchmarks/rubykon/benchmark.rb&quot;&gt;The rubykon benchmark&lt;/a&gt; (by &lt;a href=&quot;https://github.com/PragTob/rubykon&quot;&gt;@PragTob&lt;/a&gt;) is well known from the &lt;a href=&quot;https://pragtob.wordpress.com/2020/08/24/the-great-rubykon-benchmark-2020-cruby-vs-jruby-vs-truffleruby/&quot;&gt;blog&lt;/a&gt; &lt;a href=&quot;https://pragtob.wordpress.com/2017/01/24/benchmarking-a-go-ai-in-ruby-cruby-vs-rubinius-vs-jruby-vs-truffle-a-year-later/&quot;&gt;posts&lt;/a&gt; by its author.
Rubykon is an AI for the &lt;a href=&quot;https://en.wikipedia.org/wiki/Go_(game)&quot;&gt;Go board game&lt;/a&gt;.
YJIT reaches a 2.15x speedup and JRuby 2.59x. TruffleRuby is ahead with a 9.54x speedup.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;The benchmarks we just analyzed are of various size and have many different characteristics, notably some are more realistic and some less.
I think a geometric mean over their results &lt;em&gt;alone&lt;/em&gt; is not really a good summary given the variety.
But yet, it is nice to have a one-number summary for these benchmarks.
Note that CRuby 2.0 can only run 7 benchmarks out of 14, and MJIT runs 13 out of 14 (not &lt;code class=&quot;highlighter-rouge&quot;&gt;hexapdf&lt;/code&gt;).
The others Ruby versions run all 14 benchmarks.
Error bars are not meaningful on this chart.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/blog/assets/benchmarks-2022/geomean.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The chart satisfyingly goes higher from left to right.
This means, &lt;em&gt;for the geomean on those benchmarks&lt;/em&gt;, that newer CRuby versions are faster than older versions, that MJIT and YJIT are faster than without them, that JRuby’s geomean is faster than MJIT and YJIT, and that TruffleRuby is simply in a different league than other Ruby implementations with an overall 6.23x geomean speedup.&lt;/p&gt;

&lt;p&gt;On the 7 benchmarks that CRuby 2.0 can run, CRuby 3.1 MJIT is 1.74x as fast as CRuby 2.0’s geomean
(binarytrees 1.9x, erubi 1.25x, fannkuchredux 1.33x, nbody 2.17x, optcarrot 3.0x, psych-load 1.12x, rubykon 2.08x).
That’s closer to 2x than 3x (i.e., Ruby 3x3) but it is a significant improvement since CRuby 2.0.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;I think the numbers speak for themselves.&lt;/p&gt;

&lt;p&gt;My take on these results (as the TruffleRuby lead), is that TruffleRuby is able to understand and optimize Ruby code significantly better than any other implementation of Ruby.
It seems unlikely other Ruby implementations would ever reach TruffleRuby’s level of performance.
TruffleRuby uses the GraalVM JIT compiler, one of &lt;a href=&quot;https://www.graalvm.org/java/&quot;&gt;the most advanced JIT compilers available today&lt;/a&gt;. TruffleRuby, through the Truffle framework, is in direct communication with the JIT (and so are the TruffleRuby and GraalVM Compiler teams) and can easily tell the JIT what is worth optimizing/profiling/inlining and what is not, and when it should deoptimize to the interpreter to reprofile or recompile differently.
The Truffle framework makes it much easier to optimize many parts of Ruby.
As an example, adding an inline cache is just 2 lines of code (and so TruffleRuby has &amp;gt;100 inline caches), while in other VMs few inline caches are used because of the large complexity involved to add a single one.
Finally, the GraalVM architecture makes it possible to inline and just-in-time compile together Ruby code, Java code, C extensions, &lt;a href=&quot;https://rubykaigi.org/2021-takeout/presentations/eregontp.html&quot;&gt;Ruby regular expressions&lt;/a&gt; and &lt;a href=&quot;https://www.graalvm.org/docs/introduction/&quot;&gt;any other language implemented on GraalVM&lt;/a&gt;, which brings this awesome performance.&lt;/p&gt;</content><author><name>Benoit Daloze</name></author><summary type="html">In this blog post we benchmark many Ruby versions and the latest Ruby Just-in-Time compilers (JITs) on the newest Ruby benchmark suite, yjit-bench. As a teaser, the geometric mean speedups compared to CRuby 3.1 on these 14 benchmarks are: MJIT 1.26x, YJIT 1.39x, JRuby 1.86x and TruffleRuby 6.23x. Read on to find more about the benchmarks and gain insights on these speedups. This blog post is also available on Medium.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://eregon.me/blog/assets/benchmarks-2022/banner.png" /></entry><entry><title type="html">Review: Ruby Installers and Ruby Switchers</title><link href="https://eregon.me/blog/2021/06/04/review-of-ruby-installers-and-switchers.html" rel="alternate" type="text/html" title="Review: Ruby Installers and Ruby Switchers" /><published>2021-06-04T00:00:00+02:00</published><updated>2021-06-04T00:00:00+02:00</updated><id>https://eregon.me/blog/2021/06/04/review-of-ruby-installers-and-switchers</id><content type="html" xml:base="https://eregon.me/blog/2021/06/04/review-of-ruby-installers-and-switchers.html">&lt;p&gt;In this post I review the most popular Ruby installers (making it easier to install a Ruby)
and Ruby switchers (to switch between different Rubies conveniently).&lt;/p&gt;

&lt;p&gt;I contributed to all 3 Ruby installers when adding support for &lt;a href=&quot;https://www.graalvm.org/ruby/&quot;&gt;TruffleRuby&lt;/a&gt;,
and so I have experience both with their codebase and their usage.&lt;/p&gt;

&lt;p&gt;I am not reviewing Ruby installers or switchers for Windows as I have no experience with them.&lt;/p&gt;

&lt;p&gt;TLDR: feel free to jump to the &lt;a href=&quot;#conclusion-and-recommendation&quot;&gt;Conclusion and Recommendation&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;ruby-installers&quot;&gt;Ruby Installers&lt;/h2&gt;

&lt;h3 id=&quot;rvm&quot;&gt;RVM&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/rvm/rvm&quot;&gt;RVM&lt;/a&gt; is probably the oldest and certainly the
Ruby installer with the most features.
That’s actually a doubled-edged sword, as it means RVM has a much larger codebase
than all the others (25000 SLOC of Bash), and that makes it hard to maintain and
to keep correct (note: I’m not counting test SLOC).
Some features like gemsets seem much less needed nowadays as there is Bundler.&lt;/p&gt;

&lt;p&gt;To give an idea, when I &lt;a href=&quot;https://github.com/rvm/rvm/issues/4297&quot;&gt;added support&lt;/a&gt; for TruffleRuby in RVM,
I had many uncertainties, and the large codebase plus a lot of global variables made it really tricky.
As a result, there were multiple bugs and a lot more effort than for other Ruby installers.
Also, RVM actually &lt;a href=&quot;https://github.com/rvm/rvm/issues/4408&quot;&gt;changes&lt;/a&gt; standard Ruby executables like &lt;code class=&quot;highlighter-rouge&quot;&gt;rake&lt;/code&gt;,
which I really dislike as a Ruby implementer because it tends to break things in subtle ways (this behavior was later disabled when installing TruffleRuby).&lt;/p&gt;

&lt;p&gt;To be fair, RVM served me well at the time other Ruby installers/switchers did not exist.&lt;/p&gt;

&lt;p&gt;RVM supports a lot of &lt;a href=&quot;https://github.com/rvm/rvm/tree/master/scripts/functions/requirements&quot;&gt;platforms&lt;/a&gt;,
as well as &lt;a href=&quot;https://github.com/rvm/rvm/blob/master/config/known_strings&quot;&gt;ancient Rubies&lt;/a&gt;.
More platforms can be nice for beginners, but platform-specific code eventually always breaks due to incompatible changes in operating systems (e.g., package names).&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Most features&lt;/li&gt;
  &lt;li&gt;Automatically installs system packages for many platforms&lt;/li&gt;
  &lt;li&gt;Support the most Rubies, even very old ones (some with patches)&lt;/li&gt;
  &lt;li&gt;Binary builds of some CRuby versions on some platforms&lt;/li&gt;
  &lt;li&gt;Supports building &lt;code class=&quot;highlighter-rouge&quot;&gt;-head&lt;/code&gt; versions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The built Ruby can differ quite a bit from a standard build, which can cause extra issues&lt;/li&gt;
  &lt;li&gt;Huge (25000 SLOC of Bash), hard to maintain, and due to that many bugs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;ruby-build&quot;&gt;ruby-build&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/rbenv/ruby-build&quot;&gt;ruby-build&lt;/a&gt; is the best-maintained Ruby installer.
Active maintainers include &lt;code class=&quot;highlighter-rouge&quot;&gt;@hsbt&lt;/code&gt; from the CRuby core team, &lt;a href=&quot;https://github.com/rbenv/ruby-build/graphs/contributors&quot;&gt;others&lt;/a&gt; and myself.&lt;/p&gt;

&lt;p&gt;It offers a &lt;a href=&quot;https://github.com/rbenv/ruby-build/tree/master/share/ruby-build&quot;&gt;wide choice&lt;/a&gt; of Ruby versions.
It also offers &lt;code class=&quot;highlighter-rouge&quot;&gt;-dev&lt;/code&gt; versions, which is incredibly useful to let users check Ruby implementation bug fixes, or simply to try the latest/fastest/best build of a Ruby implementation.&lt;/p&gt;

&lt;p&gt;The design is really straightforward (one definition per Ruby version), and as a result it’s much more reliable.
Users update &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-build&lt;/code&gt; regularly as that is the way to get new versions. This is great to also ship bug fixes quickly.
The release process is entirely automated (kudos to &lt;code class=&quot;highlighter-rouge&quot;&gt;@mislav&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-build&lt;/code&gt; does not install system packages for you,
but documents them on &lt;a href=&quot;https://github.com/rbenv/ruby-build/wiki&quot;&gt;their wiki&lt;/a&gt;, which I find is much more future-proof.&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Actively maintained&lt;/li&gt;
  &lt;li&gt;Small, 1390 SLOC of Bash&lt;/li&gt;
  &lt;li&gt;Supports building &lt;code class=&quot;highlighter-rouge&quot;&gt;-dev&lt;/code&gt; versions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Does not install system packages on its own (but that is more future-proof, and building Rubies tend to fail clearly when there is missing dependency)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;ruby-install&quot;&gt;ruby-install&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/postmodern/ruby-install&quot;&gt;ruby-install&lt;/a&gt; is the simplest Ruby installer.&lt;/p&gt;

&lt;p&gt;It has an interesting design where installing new versions does not require to update &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt;.
Instead, &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install --latest&lt;/code&gt; is able to fetch the latest versions from &lt;a href=&quot;https://github.com/postmodern/ruby-versions&quot;&gt;ruby-versions&lt;/a&gt; and install them.
This is another double-edged sword.
On one hand it’s very elegant, and super easy to update.
On the other hand, if any other change is needed (e.g., different system packages, the download URL changed, etc), that actually requires &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt; changes, and users seem less used to update &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt;.
&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-versions&lt;/code&gt; only contains Ruby versions and checksums, not URLs or packages, which IMHO feels too limited.&lt;/p&gt;

&lt;p&gt;As an example, even though I made a PR to address a changed dependency the day it was reported,
it took &lt;a href=&quot;https://github.com/postmodern/ruby-install/issues/360&quot;&gt;8 months&lt;/a&gt; to have a &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt; release with that (to be fair, there was an easy workaround).&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt; does not support development builds by design, which is IMHO a big limitation.&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Tiny, 866 SLOC of Bash&lt;/li&gt;
  &lt;li&gt;Elegant design&lt;/li&gt;
  &lt;li&gt;Super easy to install the latest Ruby releases, if they don’t require &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt; changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Does not support &lt;code class=&quot;highlighter-rouge&quot;&gt;-dev&lt;/code&gt; builds&lt;/li&gt;
  &lt;li&gt;Infrequent releases&lt;/li&gt;
  &lt;li&gt;Users are not used to update &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Cuts a few corners in the name of simplicity&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;ruby-switchers&quot;&gt;Ruby Switchers&lt;/h2&gt;

&lt;h3 id=&quot;rvm-1&quot;&gt;RVM&lt;/h3&gt;

&lt;p&gt;RVM is both a Ruby installer and Ruby switcher so it appears here too.
The points above really apply to both so I will not repeat them here.&lt;/p&gt;

&lt;p&gt;The gemset feature tends to IMHO add more complexity than it helps, by making switching a 2-dimensional operation (ruby, gemset) instead of just 1 dimension (ruby).
Also gemsets have symlimks in their path (not expanded, that’s how RVM knows which gemset is active), and this can occasionally cause &lt;a href=&quot;https://github.com/rvm/rvm/issues/4425&quot;&gt;issues&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Most features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Overrides &lt;code class=&quot;highlighter-rouge&quot;&gt;cd&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Loads thousands of lines of Bash in your shell&lt;/li&gt;
  &lt;li&gt;Many features which are of little use nowadays&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;rbenv&quot;&gt;rbenv&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/rbenv/rbenv&quot;&gt;rbenv&lt;/a&gt; is a simple Ruby switcher.
It uses small &lt;a href=&quot;https://medium.com/@Sudhagar/rbenv-how-it-works-e5a0e4fa6e76&quot;&gt;shims&lt;/a&gt; (executables) to always redirect to the currently-selected Ruby.
There is an overhead of that, in the order of 11 ms on my machine (pretty small, considering that &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby -e 0&lt;/code&gt; takes 38 ms on my machine for ruby-3.0.1).
OTOH, it guarantees it will never pick the wrong Ruby which is obviously important.&lt;/p&gt;

&lt;p&gt;For me, the killer feature of &lt;code class=&quot;highlighter-rouge&quot;&gt;rbenv&lt;/code&gt; is it does not mess with RubyGems.
Specifically, it does not set &lt;code class=&quot;highlighter-rouge&quot;&gt;GEM_HOME&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;GEM_PATH&lt;/code&gt; (unlike the other Ruby switchers), and
so the gems corresponding to the selected Ruby are always correctly used.
That also means it expects that Rubies are installed in user-writable locations (or one uses &lt;code class=&quot;highlighter-rouge&quot;&gt;gem install --user-install&lt;/code&gt; or set Bundler &lt;code class=&quot;highlighter-rouge&quot;&gt;path&lt;/code&gt;),
but I think that’s a reasonable assumption for a Ruby switcher.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/asdf-vm/asdf&quot;&gt;asdf&lt;/a&gt; is a multi-language switcher with a very similar design to &lt;code class=&quot;highlighter-rouge&quot;&gt;rbenv&lt;/code&gt; and it uses &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Just works&lt;/li&gt;
  &lt;li&gt;Also works for non-Bash shells like &lt;a href=&quot;https://fishshell.com/&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;fish&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Reliable, does not mess with &lt;code class=&quot;highlighter-rouge&quot;&gt;GEM_HOME&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Well maintained, least number of open issues&lt;/li&gt;
  &lt;li&gt;Small, 1500 SLOC of Bash&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Small overhead on startup (~11 ms) due to shims&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;which ruby-related-executable&lt;/code&gt; is not enough to show the actual executable path, one needs &lt;code class=&quot;highlighter-rouge&quot;&gt;rbenv which&lt;/code&gt; instead&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;chruby&quot;&gt;chruby&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/postmodern/chruby&quot;&gt;chruby&lt;/a&gt; is a minimal Ruby switcher.
In fact, it’s so small it fits in a &lt;a href=&quot;https://github.com/postmodern/chruby/blob/master/share/chruby/chruby.sh&quot;&gt;100 lines of Bash&lt;/a&gt;.
After all, switching Rubies only requires changing &lt;code class=&quot;highlighter-rouge&quot;&gt;PATH&lt;/code&gt; and that’s it.&lt;/p&gt;

&lt;p&gt;Unfortunately, &lt;code class=&quot;highlighter-rouge&quot;&gt;chruby&lt;/code&gt; does one additional thing which is to set &lt;code class=&quot;highlighter-rouge&quot;&gt;GEM_HOME&lt;/code&gt;.
That can be convenient to install gems when a Ruby is installed in a non user-writable location.
Unfortunately the way it’s done is &lt;a href=&quot;https://github.com/postmodern/chruby/issues/451&quot;&gt;incorrect&lt;/a&gt; for non-CRuby.
I made a PR to fix this but 18 months after it was merged there is still no release.
So at this point I feel very disappointed about the maintenance of chruby.&lt;/p&gt;

&lt;p&gt;I still like &lt;code class=&quot;highlighter-rouge&quot;&gt;chruby&lt;/code&gt; though, and I use &lt;a href=&quot;https://github.com/postmodern/chruby/compare/master...eregon:do-no-set-gem-home&quot;&gt;a branch&lt;/a&gt; which simply doesn’t set &lt;code class=&quot;highlighter-rouge&quot;&gt;GEM_HOME&lt;/code&gt;.
That’s just &lt;a href=&quot;https://github.com/eregon/chruby/blob/do-no-set-gem-home/share/chruby/chruby.sh&quot;&gt;75 SLOC of Bash&lt;/a&gt;, one cannot get much simpler for switching Rubies.
This is probably the most future-proof Ruby switcher out there.&lt;/p&gt;

&lt;p&gt;FWIW, I made another PR to &lt;code class=&quot;highlighter-rouge&quot;&gt;chruby&lt;/code&gt; to use a similar approach, but still set &lt;code class=&quot;highlighter-rouge&quot;&gt;GEM_HOME&lt;/code&gt; if the default Gem home is not writable.
After lots of efforts, and even getting consensus on the related &lt;a href=&quot;https://github.com/rubygems/rubygems/pull/2847&quot;&gt;RubyGems issue&lt;/a&gt; for the general approach,
&lt;a href=&quot;https://github.com/postmodern/chruby/pull/431&quot;&gt;the PR&lt;/a&gt; is still not merged so I have not much hope for that (maybe it is time to hard fork &lt;code class=&quot;highlighter-rouge&quot;&gt;chruby&lt;/code&gt; …).&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Very simple and minimal, 149 SLOC of Bash&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Sets &lt;code class=&quot;highlighter-rouge&quot;&gt;GEM_HOME&lt;/code&gt; incorrectly on non-CRuby&lt;/li&gt;
  &lt;li&gt;Requires to be loaded in the shell (there is &lt;a href=&quot;https://github.com/JeanMertz/chruby-fish&quot;&gt;chruby-fish&lt;/a&gt; though)&lt;/li&gt;
  &lt;li&gt;Too rare releases, not addressing bugs promptly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;conclusion-and-recommendation&quot;&gt;Conclusion and Recommendation&lt;/h2&gt;

&lt;p&gt;In summary:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;I recommend to use &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-build&lt;/code&gt; over RVM or &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt; as IMHO it is a better Ruby installer in about every way.&lt;/li&gt;
  &lt;li&gt;I recommend to use &lt;code class=&quot;highlighter-rouge&quot;&gt;rbenv&lt;/code&gt; over RVM or &lt;code class=&quot;highlighter-rouge&quot;&gt;chruby&lt;/code&gt; as it is the only Ruby switcher not messing with gem paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;rbenv&lt;/code&gt;/&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-build&lt;/code&gt; combo is fairly natural and clearly the best maintained.&lt;/p&gt;

&lt;p&gt;Alternatively, you can try &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-build&lt;/code&gt; + my &lt;a href=&quot;https://github.com/postmodern/chruby/compare/master...eregon:do-no-set-gem-home&quot;&gt;branch of &lt;code class=&quot;highlighter-rouge&quot;&gt;chruby&lt;/code&gt; which does not set &lt;code class=&quot;highlighter-rouge&quot;&gt;GEM_HOME&lt;/code&gt;&lt;/a&gt;.
This is what I use and it works perfectly from my experience.&lt;/p&gt;

&lt;p&gt;I would recommend against RVM because of the sheer complexity and the huge codebase which is basically impossible to keep bug-free.&lt;/p&gt;

&lt;p&gt;Note that while I might criticize various softwares in this blog post,
I have a lot of respect for their authors, and
I know it is a hard job to maintain such software (written in Bash, one of the worst programming languages).&lt;/p&gt;</content><author><name>Benoit Daloze</name></author><summary type="html">In this post I review the most popular Ruby installers (making it easier to install a Ruby) and Ruby switchers (to switch between different Rubies conveniently).</summary></entry><entry><title type="html">Correct Delegation with Ruby 2.6, 2.7 and 3.0</title><link href="https://eregon.me/blog/2021/02/13/correct-delegation-in-ruby-2-27-3.html" rel="alternate" type="text/html" title="Correct Delegation with Ruby 2.6, 2.7 and 3.0" /><published>2021-02-13T00:00:00+01:00</published><updated>2021-02-13T00:00:00+01:00</updated><id>https://eregon.me/blog/2021/02/13/correct-delegation-in-ruby-2-27-3</id><content type="html" xml:base="https://eregon.me/blog/2021/02/13/correct-delegation-in-ruby-2-27-3.html">&lt;p&gt;While looking at how a few gems handle delegation on Ruby 2.7, I noticed that many of them are unfortunately incorrect.
The &lt;a href=&quot;https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/&quot;&gt;official blog post&lt;/a&gt;
about keyword arguments changes in Ruby 2.7 and Ruby 3.0 is rather long and might be unclear.
So I will keep this one really short and to the point.&lt;/p&gt;

&lt;p&gt;I am a CRuby committer which discussed and designed the keyword arguments changes along with Jeremy Evans and Yusuke Endoh.&lt;/p&gt;

&lt;h2 id=&quot;the-only-correct-way&quot;&gt;The Only Correct Way&lt;/h2&gt;

&lt;p&gt;There is actually just a single way to do delegation that works in Ruby &amp;lt;= 2.6, in Ruby 2.7 and in Ruby &amp;gt;= 3:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ruby2_keywords&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:foo&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;respond_to?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:ruby2_keywords&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s simple: &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; is the only way to achieve correct delegation on Ruby 2.7.
The approach above is used in &lt;a href=&quot;https://github.com/rails/rails/blob/v6.1.0/actionpack/lib/action_dispatch/middleware/stack.rb#L94&quot;&gt;Rails&lt;/a&gt;, etc.&lt;/p&gt;

&lt;p&gt;You can also add the &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; gem to remove the &lt;code class=&quot;highlighter-rouge&quot;&gt;respond_to?&lt;/code&gt; check and be able to use:&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;ruby2_keywords&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;some-frequent-incorrect-ways&quot;&gt;Some Frequent Incorrect Ways&lt;/h2&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Broken on 2.6 and 2.7&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;is &lt;a href=&quot;https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html#ruby-3-style-delegation&quot;&gt;incorrect on Ruby 2.6 and on Ruby 2.7&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Broken on 2.7 and 3.0&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;is &lt;a href=&quot;https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/#other-minor-changes-empty-hash&quot;&gt;incorrect on Ruby 2.7&lt;/a&gt; and &lt;a href=&quot;https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html#ruby-2-style-delegation&quot;&gt;on Ruby 3.0&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Some try a combination of the above:&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Broken on 2.7&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RUBY_VERSION&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;2.7&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;But that’s still &lt;a href=&quot;https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html#ruby-3-style-delegation&quot;&gt;incorrect on Ruby 2.7&lt;/a&gt;
(like the first example, &lt;code class=&quot;highlighter-rouge&quot;&gt;foo({})&lt;/code&gt; passes no arguments at all).&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Use &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; for delegation, that’s the only correct way, as shown above.&lt;/p&gt;

&lt;p&gt;If you maintain some Ruby code or a gem, search for methods delegating (= receiving + passing) keyword arguments with &lt;code class=&quot;highlighter-rouge&quot;&gt;**&lt;/code&gt;,
like the first and third incorrect patterns.
Delegation with &lt;code class=&quot;highlighter-rouge&quot;&gt;**&lt;/code&gt; does not work in Ruby 2.6 and 2.7, use &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; + &lt;code class=&quot;highlighter-rouge&quot;&gt;*args&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;All methods delegating with &lt;code class=&quot;highlighter-rouge&quot;&gt;*args&lt;/code&gt; should use &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; for Ruby &amp;gt;= 2.7, as long as the target method might have keyword arguments.&lt;/p&gt;

&lt;h2 id=&quot;post-scriptum&quot;&gt;Post Scriptum&lt;/h2&gt;

&lt;p&gt;Needing to use &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; explicitly for delegation is unfortunate, I wish there would be a more natural way to express delegation in Ruby 2.7.
Unfortunately there is not.&lt;/p&gt;

&lt;p&gt;I once &lt;a href=&quot;https://bugs.ruby-lang.org/issues/16463&quot;&gt;proposed&lt;/a&gt; to enable &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; by default to preserve compatibility, but this was rejected.
It would also have provided better warnings for delegation.
We could have postponed the delegation-related changes in order to migrate delegation code once (&lt;code class=&quot;highlighter-rouge&quot;&gt;*args&lt;/code&gt; -&amp;gt; &lt;code class=&quot;highlighter-rouge&quot;&gt;*args, **kwargs&lt;/code&gt;) and not twice (&lt;code class=&quot;highlighter-rouge&quot;&gt;*args&lt;/code&gt; -&amp;gt; &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; -&amp;gt; &lt;code class=&quot;highlighter-rouge&quot;&gt;*args, **kwargs&lt;/code&gt; / &lt;code class=&quot;highlighter-rouge&quot;&gt;(...)&lt;/code&gt;),
but matz wanted to make all keyword arguments deprecations at once.&lt;/p&gt;

&lt;p&gt;At least now we have Ruby 3.0, so one can more easily find problematic delegation code as it just breaks on Ruby 3.0 if not done correctly.
Note that keyword argument warnings are disabled by default in Ruby 2.7.2.
Use &lt;code class=&quot;highlighter-rouge&quot;&gt;RUBYOPT=-W:deprecated command&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;Warning[:deprecated] = true&lt;/code&gt; in Ruby code to see them.&lt;/p&gt;

&lt;p&gt;There is the &lt;code class=&quot;highlighter-rouge&quot;&gt;def foo(...); target(...); end&lt;/code&gt; syntax but it fails to parse on Ruby &amp;lt;= 2.6,
and only works with no other arguments at all in Ruby 2.7.0 - 2.7.2,
therefore it has extremely limited applicability these days and I just ignore it.
Note that &lt;code class=&quot;highlighter-rouge&quot;&gt;(arg, ...)&lt;/code&gt; might finally get &lt;a href=&quot;https://bugs.ruby-lang.org/issues/16378&quot;&gt;backported to 2.7.3&lt;/a&gt;.
It might make sense to consider &lt;code class=&quot;highlighter-rouge&quot;&gt;...&lt;/code&gt; when dropping Ruby 2.6 support.&lt;/p&gt;

&lt;p&gt;When dropping Ruby 2 support entirely, one can switch to &lt;code class=&quot;highlighter-rouge&quot;&gt;(*args, **kwargs)&lt;/code&gt;-style delegation, as that works correctly on Ruby &amp;gt;= 3.&lt;/p&gt;

&lt;p&gt;Another way would be to explicitly not support Ruby 2.7, then the last example with the version check would work.
It is probably impractical for most gems to do that though.&lt;/p&gt;</content><author><name>Benoit Daloze</name></author><summary type="html">While looking at how a few gems handle delegation on Ruby 2.7, I noticed that many of them are unfortunately incorrect. The official blog post about keyword arguments changes in Ruby 2.7 and Ruby 3.0 is rather long and might be unclear. So I will keep this one really short and to the point.</summary></entry><entry><title type="html">The Ruby Spec Suite Compatibility Report</title><link href="https://eregon.me/blog/2020/06/27/ruby-spec-compatibility-report.html" rel="alternate" type="text/html" title="The Ruby Spec Suite Compatibility Report" /><published>2020-06-27T00:00:00+02:00</published><updated>2020-06-27T00:00:00+02:00</updated><id>https://eregon.me/blog/2020/06/27/ruby-spec-compatibility-report</id><content type="html" xml:base="https://eregon.me/blog/2020/06/27/ruby-spec-compatibility-report.html">&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;For a long time I wanted to show visually the compatibility of Ruby implementations with &lt;a href=&quot;https://github.com/ruby/spec&quot;&gt;ruby/spec&lt;/a&gt;.
This is not as easy as it sounds.
First, we would like the same version of ruby/spec for all tested Ruby implementations, but in general each Ruby implementation uses a different version (except when synchronized once a month).
We would like the same Ruby compatibility version (e.g., 2.6), but some Ruby implementations do not target 2.6 currently.
And finally the number of specs (that is, of &lt;code class=&quot;highlighter-rouge&quot;&gt;it&lt;/code&gt; examples) on a given platform should be fairly constant, which required a few fixes in ruby/spec.&lt;/p&gt;

&lt;p&gt;As a quick recap, &lt;a href=&quot;https://github.com/ruby/spec&quot;&gt;ruby/spec&lt;/a&gt; and &lt;a href=&quot;https://github.com/ruby/ruby/tree/master/test/ruby&quot;&gt;CRuby tests&lt;/a&gt; are the two major test suites for Ruby compatibility.
ruby/spec has the advantage that each spec example has a description of a specific behavior it tests.
CRuby tests on the other hand often require to reverse-engineer a test to understand what behavior(s) it intends to test, and they are more coarse-grained.
In this post I focus on ruby/spec.&lt;/p&gt;

&lt;h2 id=&quot;approach&quot;&gt;Approach&lt;/h2&gt;

&lt;p&gt;In this first report I ran the specs on CRuby 2.6.6, &lt;a href=&quot;https://github.com/oracle/truffleruby&quot;&gt;TruffleRuby&lt;/a&gt;, &lt;a href=&quot;https://www.jruby.org/&quot;&gt;JRuby&lt;/a&gt;, &lt;a href=&quot;https://opalrb.com/&quot;&gt;Opal&lt;/a&gt; and &lt;a href=&quot;https://www.artichokeruby.org/&quot;&gt;Artichoke&lt;/a&gt; on Linux.
I used development versions because I want to use the same specs for all implementations when possible,
along with the corresponding tags – that is specs marked as failing –
which are only available from the corresponding repository’s development branch.&lt;/p&gt;

&lt;p&gt;The specific development versions and command lines used are shown in &lt;a href=&quot;https://gist.github.com/eregon/fc24f74752322d88a9de78c7b0e76284&quot;&gt;this gist&lt;/a&gt;.
We use the number of examples that CRuby runs, which is always equal or slightly higher than the number of examples when running on other implementations.&lt;/p&gt;

&lt;p&gt;The setup for running specs on Opal and on Artichoke is more complex – due to typically not having access to the real filesystem – so I used the version of ruby/spec that they import, which is older and therefore has less specs.
Opal currently targets Ruby 2.5 and Artichoke is still an early Ruby implementation.
They both use an &lt;a href=&quot;https://github.com/opal/opal/blob/master/spec/ruby_specs&quot;&gt;include list&lt;/a&gt; of &lt;a href=&quot;https://github.com/artichoke/artichoke/blob/master/spec-runner/enforced-specs.yaml&quot;&gt;specs&lt;/a&gt; to run, and so might pass more than tested in CI.
So please consider numbers for those Ruby implementations with a grain of salt.&lt;/p&gt;

&lt;p&gt;Below you can see two totals, because some implementations do not support the Ruby C-API:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;All specs except C-API specs.&lt;/li&gt;
  &lt;li&gt;All specs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without further ado, here is the data.&lt;/p&gt;

&lt;h2 id=&quot;results&quot;&gt;Results&lt;/h2&gt;

&lt;table style=&quot;width: 100%&quot;&gt;
&lt;colgroup&gt;
&lt;col span=&quot;1&quot; style=&quot;width: 16%;&quot; /&gt;
&lt;col span=&quot;1&quot; style=&quot;width: 16%&quot; /&gt;
&lt;col span=&quot;1&quot; style=&quot;width: 16%&quot; /&gt;
&lt;col span=&quot;1&quot; style=&quot;width: 16%&quot; /&gt;
&lt;col span=&quot;1&quot; style=&quot;width: 16%&quot; /&gt;
&lt;col span=&quot;1&quot; style=&quot;width: 16%&quot; /&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th style=&quot;text-align: center&quot;&gt;CRuby&lt;/th&gt;
&lt;th style=&quot;text-align: center&quot;&gt;TruffleRuby&lt;/th&gt;
&lt;th style=&quot;text-align: center&quot;&gt;JRuby&lt;/th&gt;
&lt;th style=&quot;text-align: center&quot;&gt;Opal&lt;/th&gt;
&lt;th style=&quot;text-align: center&quot;&gt;Artichoke&lt;/th&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/ruby/spec/tree/master/command_line&quot;&gt;Command-line&lt;/a&gt;&lt;br /&gt;

          141 specs&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;100.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;100%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;87.94326241134752, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;87.94%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;73.75886524822694, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;73.76%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;0.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;0.00%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;0.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;0.00%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/ruby/spec/tree/master/language&quot;&gt;Language&lt;/a&gt;&lt;br /&gt;

          2367 specs&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;100.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;100%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;97.84537389100126, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;97.85%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;97.16941275876637, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;97.17%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;69.83523447401775, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;69.84%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;0.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;0.00%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/ruby/spec/tree/master/core&quot;&gt;Core Library&lt;/a&gt;&lt;br /&gt;

          20841 specs&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;100.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;100%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;96.80437598963582, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;96.80%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;93.61355021352142, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;93.61%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;42.934600067175275, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;42.93%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;6.5591862194712345, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;6.56%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/ruby/spec/tree/master/library&quot;&gt;Standard Library&lt;/a&gt;&lt;br /&gt;

          6891 specs&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;100.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;100%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;97.62008416775504, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;97.62%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;85.96720359889711, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;85.97%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;7.255840951966333, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;7.26%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;4.745319982585982, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;4.75%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/ruby/spec/tree/master/security&quot;&gt;Security&lt;/a&gt;&lt;br /&gt;

          40 specs&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;100.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;100%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;100.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;100%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;92.5, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;92.50%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;0.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;0.00%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;0.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;0.00%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;border-top: 2px solid black&quot;&gt;
&lt;td&gt;&lt;b&gt;TOTAL without C-API specs&lt;/b&gt;&lt;br /&gt;

          30280 specs&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;100.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;100%&lt;/text&gt;
    &lt;/svg&gt;
&lt;span style=&quot;font-size: 95%&quot;&gt;30280 passing&lt;/span&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;97.0343461030383, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;97.03%&lt;/text&gt;
    &lt;/svg&gt;
&lt;span style=&quot;font-size: 95%&quot;&gt;29382 passing&lt;/span&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;92.05746367239101, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;92.06%&lt;/text&gt;
    &lt;/svg&gt;
&lt;span style=&quot;font-size: 95%&quot;&gt;27875 passing&lt;/span&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;36.66116248348745, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;36.66%&lt;/text&gt;
    &lt;/svg&gt;
&lt;span style=&quot;font-size: 95%&quot;&gt;11101 passing&lt;/span&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;5.594451783355351, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;5.59%&lt;/text&gt;
    &lt;/svg&gt;
&lt;span style=&quot;font-size: 95%&quot;&gt;1694 passing&lt;/span&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/ruby/spec/tree/master/optional/capi&quot;&gt;C-API&lt;/a&gt;&lt;br /&gt;

          1325 specs&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;100.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;100%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;97.35849056603773, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;97.36%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;0.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;0.00%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;0.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;0.00%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;0.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;0.00%&lt;/text&gt;
    &lt;/svg&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;border-top: 2px solid black&quot;&gt;
&lt;td&gt;&lt;b&gt;TOTAL&lt;/b&gt;&lt;br /&gt;

          31605 specs&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;100.0, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;100%&lt;/text&gt;
    &lt;/svg&gt;
&lt;span style=&quot;font-size: 95%&quot;&gt;31605 passing&lt;/span&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;97.04793545325107, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;97.05%&lt;/text&gt;
    &lt;/svg&gt;
&lt;span style=&quot;font-size: 95%&quot;&gt;30672 passing&lt;/span&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;88.19806992564467, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;88.20%&lt;/text&gt;
    &lt;/svg&gt;
&lt;span style=&quot;font-size: 95%&quot;&gt;27875 passing&lt;/span&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;35.12418921056795, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;35.12%&lt;/text&gt;
    &lt;/svg&gt;
&lt;span style=&quot;font-size: 95%&quot;&gt;11101 passing&lt;/span&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center&quot;&gt;
    &lt;svg viewBox=&quot;0 0 36 36&quot; class=&quot;circular-chart&quot;&gt;
      &lt;path class=&quot;circle-bg&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;path class=&quot;circle&quot; stroke-dasharray=&quot;5.359911406423034, 100&quot; d=&quot;M18 2.0845
          a 15.9155 15.9155 0 0 1 0 31.831
          a 15.9155 15.9155 0 0 1 0 -31.831&quot; /&gt;
      &lt;text x=&quot;18&quot; y=&quot;20.35&quot; class=&quot;percentage&quot;&gt;5.36%&lt;/text&gt;
    &lt;/svg&gt;
&lt;span style=&quot;font-size: 95%&quot;&gt;1694 passing&lt;/span&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;interpretation&quot;&gt;Interpretation&lt;/h2&gt;

&lt;p&gt;The interpretation that follows is mine, and I tried to keep it as neutral and factual as possible.
I apologize if there is any inaccuracy or subjective opinion: let me know, I am happy to update the blog post based on justified comments.&lt;/p&gt;

&lt;p&gt;As expected, CRuby passes all specs.
This is by design, ruby/spec only includes specs that pass on CRuby since it is the standard implementation.
There is a rare exception for specs specifying the correct behavior for known bugs in CRuby (&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby_bug&lt;/code&gt; in MSpec) which we can ignore here (there are only 3 &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby_bug&lt;/code&gt; specs currently in ruby/spec).&lt;/p&gt;

&lt;p&gt;We see that TruffleRuby passes 97% of all specs which is quite impressive.
To be fair, TruffleRuby focuses more on ruby/spec than on CRuby tests for the advantages mentioned above.&lt;/p&gt;

&lt;p&gt;Next, we have JRuby passing 88% or 92% of all specs, depending on whether you count C-API specs.
That’s impressive as well, notably with high scores on language and core specs.
It is worth noting JRuby is currently working on finishing Ruby 2.6 support.&lt;/p&gt;

&lt;p&gt;Then we have Opal, which passes a sizable number of language, core and library specs.
Opal is compiling to JavaScript, so unlike JRuby and TruffleRuby it does not aim to implement lower-level system functionality. Opal is also typically run in browsers and not on the command line, so Opal does not run command-line specs.&lt;/p&gt;

&lt;p&gt;Finally, Artichoke, a fairly new Ruby implementation, passes some core and library specs.
Based on MRuby, which implements a subset of Ruby, it still has a long way to achieve compatibility with CRuby.
Update (February 2021): Artichoke now passes an estimated 22% of core library specs when trying to run all core specs with &lt;a href=&quot;https://github.com/artichoke/artichoke/pull/1088&quot;&gt;this PR&lt;/a&gt;, however those are not run in CI and might regress.&lt;/p&gt;

&lt;p&gt;Note that specs from ruby/spec are not necessarily representative of general compatibility
with the entire Ruby ecosystem, even though they are obviously related.
For instance, there might be far more spec examples on a given method than usages in Ruby gems, or the opposite.
There are Ruby semantics that are not yet covered in ruby/spec: it is an ongoing effort to improve coverage by the way of contributions to ruby/spec from Ruby implementations and from external contributors.
In general, when an alternative implementation has a bug report,
they either ensure there is already a spec or add one so over time the ruby/spec coverage improves.&lt;/p&gt;

&lt;p&gt;I took a look at CRuby tests for more context and there are in total 20587 CRuby tests when run on CRuby 2.6.6 locally.
TruffleRuby &lt;a href=&quot;https://github.com/oracle/truffleruby/commit/63ac8611de88ba57befa9f8224b4c71980d0b6ce&quot;&gt;currently&lt;/a&gt; runs and passes 10467 CRuby tests in CI.
JRuby &lt;a href=&quot;https://travis-ci.org/github/jruby/jruby/builds/702890075&quot;&gt;currently&lt;/a&gt; runs and passes 8133 CRuby tests in CI.
Opal and Artichoke do not seem to run CRuby tests currently.
So it seems clear alternative Ruby implementations focus more on ruby/spec,
but also that there are still a lot more CRuby tests they could run.
Some CRuby tests are however CRuby-specific and not applicable for any other Ruby implementation.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;I am looking forward to Ruby implementations improving their compatibility and
passing a higher percentage of ruby/spec. I plan to make such a report again when that happens.&lt;/p&gt;

&lt;h3 id=&quot;other-ruby-implementations&quot;&gt;Other Ruby Implementations&lt;/h3&gt;

&lt;p&gt;I would like to report on other Ruby implementations but currently it seems difficult.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Rubinius 5.0 &lt;a href=&quot;https://github.com/rubinius/rubinius/issues/3832&quot;&gt;fails to compile&lt;/a&gt; on my machine.
I have noticed similar issues on various Ubuntu versions too.
I used &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-build&lt;/code&gt; and installed the &lt;code class=&quot;highlighter-rouge&quot;&gt;clang llvm-devel&lt;/code&gt; packages.&lt;/li&gt;
  &lt;li&gt;MRuby 2.1.1 (as installed by &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt;) does not support &lt;code class=&quot;highlighter-rouge&quot;&gt;$:&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;require&lt;/code&gt; and so cannot run the regular MSpec harness. Also, &lt;a href=&quot;https://github.com/mruby/mruby&quot;&gt;mruby/mruby&lt;/a&gt; does not seem to run ruby/spec as part of its CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any help to run ruby/spec on these implementations with the &lt;code class=&quot;highlighter-rouge&quot;&gt;stats&lt;/code&gt; formatter or a compatible output is welcome.
The &lt;a href=&quot;https://github.com/ruby/mspec/blob/master/lib/mspec/runner/formatters/stats.rb&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;stats&lt;/code&gt; formatter&lt;/a&gt; has a very simple format, it’s a Hash of spec files to number of passing examples as a YAML file:&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;---
language/some_spec.rb:
  :examples: 10
  :errors: 1
  :failures: 2
  :tagged: 3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;acknowledgments&quot;&gt;Acknowledgments&lt;/h3&gt;

&lt;p&gt;I used the technique from this &lt;a href=&quot;https://medium.com/@pppped/how-to-code-a-responsive-circular-percentage-chart-with-svg-and-css-3632f8cd7705&quot;&gt;blog post&lt;/a&gt; to draw the percentage charts.&lt;/p&gt;</content><author><name>Benoit Daloze</name></author><summary type="html">Introduction</summary></entry><entry><title type="html">A Migration Path to Bundler 2+</title><link href="https://eregon.me/blog/2020/01/13/a-migration-path-to-bundler2.html" rel="alternate" type="text/html" title="A Migration Path to Bundler 2+" /><published>2020-01-13T00:00:00+01:00</published><updated>2020-01-13T00:00:00+01:00</updated><id>https://eregon.me/blog/2020/01/13/a-migration-path-to-bundler2</id><content type="html" xml:base="https://eregon.me/blog/2020/01/13/a-migration-path-to-bundler2.html">&lt;p&gt;Bundler 2 did not arrive quietly.
It was noticed by almost every CI build failing when running &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle install&lt;/code&gt;.
As a result, it seems many still avoid Bundler 2 and just use Bundler 1.
In this post, I present some ideas on how to get more people to use Bundler 2,
and no longer need Bundler 1 which will not be maintained forever.&lt;/p&gt;

&lt;h2 id=&quot;the-rubygems-requirement-of-bundler-2&quot;&gt;The RubyGems Requirement of Bundler 2&lt;/h2&gt;

&lt;p&gt;The original release of Bundler 2, which is version 2.0.0, &lt;a href=&quot;https://bundler.io/blog/2019/01/03/announcing-bundler-2.html&quot;&gt;required such a
recent RubyGems version&lt;/a&gt;
that none of the released Ruby versions had a recent enough RubyGems version shipped with them.
Bundler 2 also requires Ruby 2.3+, and finally drops support for Ruby 1.8.&lt;/p&gt;

&lt;p&gt;This RubyGems version requirement was quickly found as problematic, and as a result Bundler 2.0.1 was released
to lower the RubyGems version requirement to RubyGems 2.5.
All versions of Ruby supported by Bundler 2 (that is, Ruby 2.3+) ship with RubyGems 2.5 or newer, but the &lt;a href=&quot;https://bundler.io/blog/2019/01/04/an-update-on-the-bundler-2-release.html&quot;&gt;blog post&lt;/a&gt; did not make this clear.&lt;/p&gt;

&lt;p&gt;This resulted in a lot of confusion. Most repositories ended up explicitly
updating RubyGems in CI to make Bundler 2.0.0 work, as mentioned in the original blog post.&lt;/p&gt;

&lt;p&gt;To summarize, &lt;strong&gt;it is not needed to update RubyGems to use Bundler 2.0.1+&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In fact, I would even argue against updating RubyGems in CI because:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;It makes the CI slower.&lt;/li&gt;
  &lt;li&gt;It makes the CI less reliable by frequently changing the RubyGems version used.&lt;/li&gt;
  &lt;li&gt;It uses the latest RubyGems instead of the well known version bundled with that Ruby version.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;bundler-version-autoswitching&quot;&gt;Bundler Version Autoswitching&lt;/h2&gt;

&lt;p&gt;Bundler 2 came with another feature, which to say the least is very confusing.
This feature is actually implemented directly in RubyGems and it unfortunately included a bug,
which led to even &lt;a href=&quot;https://bundler.io/blog/2019/05/14/solutions-for-cant-find-gem-bundler-with-executable-bundle.html&quot;&gt;more confusion&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Supposing you have both Bundler 1.17.2 and Bundler 2.0.2 installed, what should this print?&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ bundle --version
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For every other gem, the answer would be the latest version of the gem installed, so in this case 2.0.2.&lt;/p&gt;

&lt;p&gt;The actual answer though, due to Bundler version autoswitching is “it depends on many things”.
Specifically, if the current directory or any parent directory has a &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; file with a &lt;code class=&quot;highlighter-rouge&quot;&gt;BUNDLED WITH&lt;/code&gt; section,
then that exact version, or the closest version with the same major version (depending on the RubyGems version), will be used. If such a version is not available, it fails with &lt;code class=&quot;highlighter-rouge&quot;&gt;Could not find 'bundler' (&amp;lt;VERSION&amp;gt;)&lt;/code&gt; (e.g., when only Bundler 2 is installed with a &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; &lt;code class=&quot;highlighter-rouge&quot;&gt;BUNDLED WITH 1.17.2&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Otherwise, if there is no &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; in the current directory or any parent directory, &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle&lt;/code&gt; behaves like a normal gem and uses the latest version.&lt;/p&gt;

&lt;p&gt;As an example, this feature means that as long as there is a &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; &lt;code class=&quot;highlighter-rouge&quot;&gt;BUNDLED WITH 1.x&lt;/code&gt;,
even if Bundler 2 is installed, Bundler 1 would be used, or the error above would be shown.&lt;/p&gt;

&lt;p&gt;I think this “feature” is so confusing that it should be considered a bug.
It’s counter-intuitive and prevents using Bundler 2 on &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; &lt;code class=&quot;highlighter-rouge&quot;&gt;BUNDLED WITH 1.x&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-migration-problem&quot;&gt;The Migration Problem&lt;/h2&gt;

&lt;p&gt;The problem I see with all this is I think very few people use Bundler 2, and rather they just keep using Bundler 1.
I tried to estimate how often Bundler 2 is used by various means:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;I made a &lt;a href=&quot;https://twitter.com/eregontp/status/1188046284919332864&quot;&gt;poll&lt;/a&gt; on Twitter but that got rather few responses, with rather surprising results.&lt;/li&gt;
  &lt;li&gt;Some usage statistics from RubyGems.org show that Bundler 2 is only used in about 2% of requests to RubyGems.org.&lt;/li&gt;
  &lt;li&gt;TravisCI seems to currently use Bundler 1 &lt;a href=&quot;https://travis-ci.org/eregon/mspec/builds/600094360&quot;&gt;for every Ruby version&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Most Ruby projects I have seen with a &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; are &lt;code class=&quot;highlighter-rouge&quot;&gt;BUNDLED WITH 1.x&lt;/code&gt;, showing Bundler 1 is used.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, Bundler 1 seems no longer maintained (which is fair enough, who would want to maintain Ruby 1.8 compatibility?) and likely will no longer receive bug fixes and security patches.&lt;/p&gt;

&lt;p&gt;So it is important that Rubyists migrate to Bundler 2,
but I think the current situation made it so inconvenient that very few did.
To address those concerns, I propose some ideas below to make migration to Bundler 2 easier.&lt;/p&gt;

&lt;h2 id=&quot;a-better-migration-path-for-bundler-2&quot;&gt;A Better Migration Path for Bundler 2+&lt;/h2&gt;

&lt;p&gt;Here are my suggestions to make migration to Bundler 2 easier.
I think it would be good to integrate them in future Bundler releases, including 2.x releases.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Do not require a RubyGems version newer than what Ruby versions supported by Bundler ship.
  Requiring to update RubyGems causes many troubles as seen wth Bundler 2.0.0.&lt;/li&gt;
  &lt;li&gt;Drop the version autoswitching mechanism in RubyGems for Bundler entirely. For older RubyGems versions with autoswitching, do whatever is necessary in Bundler to avoid that behavior, or at least warn.&lt;/li&gt;
  &lt;li&gt;Every future version of Bundler should support previous versions of &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Do not modify the &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; if there would be no other change than the &lt;code class=&quot;highlighter-rouge&quot;&gt;BUNDLED WITH&lt;/code&gt; version (e.g., for &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle install&lt;/code&gt; with an existing up-to-date &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This third point about backward compatibility might require extra maintenance work in Bundler.
However, I think almost every software of the scale of Bundler needs to do this kind of things,
and I would expect keeping a few extra parsers for older formats of &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; is not so much work.
For instance, even in Bundler 10, I expect installing gems from an already-resolved &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; is basically just installing every listed gem with the listed version.
Why &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt; and not just &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt;? Because Bundler evaluates the &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt; in all cases in its current design.&lt;/p&gt;

&lt;p&gt;I think supporting older &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; is the key for more people to adopt newer versions of Bundler.
And importantly, this is the only way people can use newer versions of Bundler and stop using unmaintained and old Bundler versions for existing &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; files with &lt;code class=&quot;highlighter-rouge&quot;&gt;BUNDLED WITH 1.x&lt;/code&gt; (not all &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; will be migrated immediately to Bundler 2).&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;I shared these suggestions with the Bundler team in October 2019 and they generally agreed and already discussed these subjects at RubyKaigi 2019.&lt;/p&gt;

&lt;p&gt;Some of the suggestions listed above have already been addressed.
For instance, the RubyGems version requirement (#1) was fixed in 2.0.1, one day after the 2.0.0 release.&lt;/p&gt;

&lt;p&gt;The latest RubyGems and Bundler 2 can actually already install a &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; &lt;code class=&quot;highlighter-rouge&quot;&gt;BUNDLED WITH 1.x&lt;/code&gt; (#3), but it unfortunately changes the &lt;code class=&quot;highlighter-rouge&quot;&gt;BUNDLED WITH&lt;/code&gt; section for seemingly no reason (#4 not yet). This can be seen on 2.7.0-preview2:&lt;/p&gt;

&lt;pre&gt;
$ chruby 2.7.0-preview2
$ bundle _2.1.0.pre.2_ install
...
&lt;span style=&quot;color: GoldenRod&quot;&gt;Warning: the lockfile is being updated to Bundler 2,
after which you will be unable to return to Bundler 1.&lt;/span&gt;
&lt;span style=&quot;color: green&quot;&gt;Bundle complete! 2 Gemfile dependencies, 7 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.&lt;/span&gt;
$ git diff
&lt;span style=&quot;color: red&quot;&gt;---&lt;/span&gt; Gemfile.lock
&lt;span style=&quot;color: green&quot;&gt;+++&lt;/span&gt; Gemfile.lock
 BUNDLED WITH
&lt;span style=&quot;color: red&quot;&gt;-   1.17.2&lt;/span&gt;
&lt;span style=&quot;color: green&quot;&gt;+   2.1.0.pre.2&lt;/span&gt;
&lt;/pre&gt;

&lt;p&gt;That changes is annoying.
Suppose I’m contributing to Rails and I’m just doing &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle install&lt;/code&gt; (without modifying any &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt;).
If this changes the &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; I will have to constantly undo that change, or be careful to not commit it and keep a dirty working tree.
If it is committed and merged, it would force every single contributor of Rails (and the CI) to use Bundler 2 once they pull that change.
And every such contributor, when doing &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle install&lt;/code&gt; on their own projects would also see the &lt;code class=&quot;highlighter-rouge&quot;&gt;BUNDLED WITH&lt;/code&gt; version updated, making this a cascading effect.
That sounds to me like a recipe for a really not smooth migration forcing everyone to update in a really short time, so that’s why I think Bundler should not touch the &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; if there would be no other change than the &lt;code class=&quot;highlighter-rouge&quot;&gt;BUNDLED WITH&lt;/code&gt; version (#4).&lt;/p&gt;

&lt;p&gt;On the other hand, it seems fine to use &lt;code class=&quot;highlighter-rouge&quot;&gt;BUNDLED WITH 2.x&lt;/code&gt; if there is no &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; or the dependencies need to be re-resolved due to, e.g., adding a gem, since Bundler 2 might resolve gems differently than Bundler 1.&lt;/p&gt;

&lt;p&gt;So #1 and #3 seem mostly done for the latest 2.x release, but #2 and #4 are not yet addressed.&lt;/p&gt;

&lt;p&gt;Does that sound like a good migration plan to you?&lt;/p&gt;

&lt;p&gt;Would you be willing to help making these changes in Bundler and RubyGems?&lt;/p&gt;</content><author><name>Benoit Daloze</name></author><summary type="html">Bundler 2 did not arrive quietly. It was noticed by almost every CI build failing when running bundle install. As a result, it seems many still avoid Bundler 2 and just use Bundler 1. In this post, I present some ideas on how to get more people to use Bundler 2, and no longer need Bundler 1 which will not be maintained forever.</summary></entry><entry><title type="html">The Delegation Challenge of Ruby 2.7</title><link href="https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html" rel="alternate" type="text/html" title="The Delegation Challenge of Ruby 2.7" /><published>2019-11-10T00:00:00+01:00</published><updated>2019-11-10T00:00:00+01:00</updated><id>https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27</id><content type="html" xml:base="https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html">&lt;p&gt;Ruby 3.0 will introduce the separation between positional and keyword arguments.
The upcoming Ruby 2.7 release &lt;a href=&quot;https://bugs.ruby-lang.org/issues/14183&quot;&gt;aims to introduce warnings&lt;/a&gt; for every argument
behavior that will change in Ruby 3.0 to ease migration.
However, delegation that works for Ruby 2.6, 2.7 and 3 seems a hard problem to solve.&lt;/p&gt;

&lt;p&gt;See the &lt;a href=&quot;https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/&quot;&gt;migration guide on ruby-lang.org&lt;/a&gt;
if you want to migrate your code to work on 2.7+,
and see &lt;a href=&quot;https://eregon.me/blog/2021/02/13/correct-delegation-in-ruby-2-27-3.html&quot;&gt;Correct Delegation with Ruby 2.6, 2.7 and 3.0&lt;/a&gt; regarding delegation compatible with Ruby 2.6, 2.7 and 3.0.&lt;/p&gt;

&lt;p&gt;This blog post is mostly discussing various possibilities to fix delegation in Ruby 2.7.
By now it is mostly history, only &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; is available in Ruby 2.7.&lt;/p&gt;

&lt;h2 id=&quot;what-will-change-for-arguments-in-ruby-3&quot;&gt;What will change for arguments in Ruby 3?&lt;/h2&gt;

&lt;p&gt;From Ruby 2.0 until Ruby 2.6, keyword arguments in method definitions such as
&lt;code class=&quot;highlighter-rouge&quot;&gt;def m(kwreq:, kwopt: 42, **kwrest)&lt;/code&gt; are basically just syntactic sugar
for extracting values from a &lt;code class=&quot;highlighter-rouge&quot;&gt;Hash&lt;/code&gt; passed in last position.&lt;/p&gt;

&lt;p&gt;This however leads to some issues when keyword arguments are mixed with
&lt;code class=&quot;highlighter-rouge&quot;&gt;optional = default&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;*rest&lt;/code&gt; arguments.
For instance, should &lt;code class=&quot;highlighter-rouge&quot;&gt;def m(opt = 42, **kwargs); end; m({ kw: 1 })&lt;/code&gt; pass the Hash
as keyword arguments (&lt;code class=&quot;highlighter-rouge&quot;&gt;**kwargs&lt;/code&gt;) or as the value for the optional argument (&lt;code class=&quot;highlighter-rouge&quot;&gt;opt&lt;/code&gt;)?
In Ruby 2, the answer is it passes the Hash as keyword arguments.
In Ruby 3 however, such a call would pass a Hash as positional, i.e., assign it to &lt;code class=&quot;highlighter-rouge&quot;&gt;opt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is no clear answer as to which is better, without a general rule, and the
separation between positional and keyword arguments is that rule.&lt;/p&gt;

&lt;p&gt;Actually, there already is a similar separation in Ruby for the block argument.
A block argument is always passed with &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;amp;block&lt;/code&gt; or as a literal block (&lt;code class=&quot;highlighter-rouge&quot;&gt;meth(*args) { ... }&lt;/code&gt;)
and is never mixed with other kinds of arguments.&lt;/p&gt;

&lt;p&gt;Here are some basics on what passes keyword arguments and what passes positional arguments:&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;a: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;a: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# passes kwargs in Ruby 2 and 3&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# passes kwargs in Ruby 2 and 3&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;a: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# passes kwargs in Ruby 2 and positional in Ruby 3&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;      &lt;span class=&quot;c1&quot;&gt;# passes kwargs in Ruby 2 and positional in Ruby 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Methods not taking keyword arguments can still be called with the keyword
arguments syntax. The reason here is to avoid breaking compatibility too much.
If that was not done, such a call would raise an &lt;code class=&quot;highlighter-rouge&quot;&gt;ArgumentError&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;nokwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;nokwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;a: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [{a: 1}] in both Ruby 2 and 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One important change in Ruby 2.7, is that &lt;code class=&quot;highlighter-rouge&quot;&gt;**empty_hash&lt;/code&gt; actually passes nothing:&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;empty_hash&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;nokwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;empty_hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [] in Ruby 2.7+, [{}] in Ruby 2.6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Confusingly enough, &lt;code class=&quot;highlighter-rouge&quot;&gt;**{}&lt;/code&gt; is treated specially by the parser and automatically
removed in Ruby 2.6, which means:&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;nokwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [] in Ruby 2 and 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The rationale here is &lt;code class=&quot;highlighter-rouge&quot;&gt;m(**kwargs)&lt;/code&gt;, much like &lt;code class=&quot;highlighter-rouge&quot;&gt;m(*args)&lt;/code&gt; must pass nothing if it’s empty, that is if there are no arguments to pass.
It was essentially a mistake of Ruby 2.6 and before to pass an empty Hash in such a case, but that’s how it is and we cannot change the past. Maybe it was somehow more compatible for some cases.&lt;/p&gt;

&lt;p&gt;These changes have been the work of Jeremy Evans (of Sequel fame), Yusuke Endoh (@mame) and others.
I have been mostly watching and commenting on those changes.&lt;/p&gt;

&lt;p&gt;Now that we’ve got the basics we can look at the Delegation Challenge!&lt;/p&gt;

&lt;p&gt;I will not explain in this blog post how to make non-delegation code work on Ruby 3 and how to fix the warnings of Ruby 2.7, that’s the goal of the (upcoming at the time of writing) &lt;a href=&quot;https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/&quot;&gt;migration guide on ruby-lang.org&lt;/a&gt;, which is mentioned in the &lt;a href=&quot;https://www.ruby-lang.org/en/news/2019/12/25/ruby-2-7-0-released/&quot;&gt;Ruby 2.7 release notes&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-delegation-challenge&quot;&gt;The Delegation Challenge&lt;/h2&gt;

&lt;p&gt;So now that we have a proper separation of keyword arguments, how should delegation look like?&lt;/p&gt;

&lt;h3 id=&quot;ruby-2-style-delegation&quot;&gt;Ruby 2-style Delegation&lt;/h3&gt;

&lt;p&gt;Up until Ruby 2.6, delegation had always been rather simple:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And this was enough for perfect forwarding, i.e., pass whatever arguments are passed to &lt;code class=&quot;highlighter-rouge&quot;&gt;delegate&lt;/code&gt; to &lt;code class=&quot;highlighter-rouge&quot;&gt;target&lt;/code&gt;, as if &lt;code class=&quot;highlighter-rouge&quot;&gt;target&lt;/code&gt; had been called directly.&lt;/p&gt;

&lt;p&gt;However this doesn’t quite work in Ruby 3:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;b: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [[1], {b: 2}] in Ruby 2 &amp;amp; 3&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;b: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [[1], {b: 2}] in Ruby 2, [[1, {b: 2}], {}] in Ruby 3&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Ruby 2.7:&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# warning: The last argument is used as the keyword parameter&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# warning: for `target' defined here&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [[1], {b: 2}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Because &lt;code class=&quot;highlighter-rouge&quot;&gt;delegate&lt;/code&gt; does not take keyword arguments, &lt;code class=&quot;highlighter-rouge&quot;&gt;b: 2&lt;/code&gt; is passed as positional.
In Ruby 3, it remains as positional and therefore results in different behavior than calling the method directly.
In Ruby 2.7, it warns because the behavior changes in Ruby 3.&lt;/p&gt;

&lt;h3 id=&quot;ruby-3-style-delegation&quot;&gt;Ruby 3-style Delegation&lt;/h3&gt;

&lt;p&gt;So maybe we should use this to delegate instead?&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;b: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [[1], {b: 2}] in Ruby 2 &amp;amp; 3&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;       &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [[1], {}]     in Ruby 2 &amp;amp; 3&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;b: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [[], {b: 2}]  in Ruby 2 &amp;amp; 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Alright, this seems to work fine, but what if &lt;code class=&quot;highlighter-rouge&quot;&gt;target&lt;/code&gt; does not take keyword arguments?&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; []   in Ruby 2 &amp;amp; 3&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [1]  in Ruby 2 &amp;amp; 3&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [{}]    in Ruby 2.6, []  in Ruby 2.7+&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [1, {}] in Ruby 2.6, [1] in Ruby 2.7+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now Ruby 2.6 starts to pass an extra positional Hash to &lt;code class=&quot;highlighter-rouge&quot;&gt;target&lt;/code&gt; when there are no keyword arguments!
That’s because &lt;code class=&quot;highlighter-rouge&quot;&gt;**empty_hash&lt;/code&gt; passes &lt;code class=&quot;highlighter-rouge&quot;&gt;{}&lt;/code&gt; in Ruby 2.6, and nothing in Ruby 2.7+.&lt;/p&gt;

&lt;p&gt;So neither of those &lt;code class=&quot;highlighter-rouge&quot;&gt;delegate&lt;/code&gt; methods works on both Ruby 2 and 3, that is very unfortunate.&lt;/p&gt;

&lt;p&gt;And to make matters worse Ruby 2.7 has its own behavior different from Ruby 2.6 and Ruby 3:&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;b: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;     &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; {b: 2} in Ruby 2 &amp;amp; 3&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;b: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; {b: 2} in Ruby 2 &amp;amp; 3&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# However in 2.7 emits a warning (which is unjustified, passing a positional Hash to #target is fine):&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# warning: The last argument is used as the keyword parameter&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# warning: for `delegate' defined here&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({})&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [{}] in Ruby 2.6 &amp;amp; 3, but [] in Ruby 2.7! (with a warning)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;For compatibility with 2.6, Ruby 2.7 passes &lt;code class=&quot;highlighter-rouge&quot;&gt;{}&lt;/code&gt; as a keyword argument here, but
then &lt;code class=&quot;highlighter-rouge&quot;&gt;delegate&lt;/code&gt; receives &lt;code class=&quot;highlighter-rouge&quot;&gt;args=[]&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;kwargs={}&lt;/code&gt; and call &lt;code class=&quot;highlighter-rouge&quot;&gt;target&lt;/code&gt; with &lt;code class=&quot;highlighter-rouge&quot;&gt;**kwargs&lt;/code&gt;,
which passes nothing in 2.7 and therefore drops the given Hash!&lt;/p&gt;

&lt;p&gt;We can actually convince Ruby 2.7 to pass a positional Hash with &lt;code class=&quot;highlighter-rouge&quot;&gt;, **{}&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({},&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [{}] in Ruby 2 and 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So for Ruby 2.7, neither of these approaches even works for all cases (without changing call sites to use &lt;code class=&quot;highlighter-rouge&quot;&gt;, **{}&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;So that is the Delegation Challenge:
How can we achieve perfect forwarding in Ruby 2.6, Ruby 2.7 and Ruby 3+ and avoid warnings from Ruby 2.7?&lt;/p&gt;

&lt;h2 id=&quot;possible-solutions&quot;&gt;Possible Solutions&lt;/h2&gt;

&lt;p&gt;There are multiple possibilities:&lt;/p&gt;

&lt;h3 id=&quot;version-check-with-ruby-2--ruby-3-style-delegation&quot;&gt;Version check with Ruby 2 + Ruby 3-style delegation&lt;/h3&gt;

&lt;p&gt;We could use a version check like:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RUBY_VERSION&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;3&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;This would work fine in Ruby 2.0-2.6 and Ruby 3+.
Unfortunately it does not work in Ruby 2.7 which has behavior “in between” Ruby 2.6 and Ruby 3 (&lt;code class=&quot;highlighter-rouge&quot;&gt;**empty_hash&lt;/code&gt; passes nothing but positional Hash are still converted to keyword arguments like in 2.6).
We’d probably still want to be able to run the code on Ruby 2.7 to get the migration warnings to help migrating to Ruby 3. For that, we would need to address delegation on Ruby 2.7, otherwise there would be a lot of false-positive warnings.&lt;/p&gt;

&lt;p&gt;Maybe using Ruby 3-style delegation and adding &lt;code class=&quot;highlighter-rouge&quot;&gt;, **{}&lt;/code&gt; at call sites which need it for Ruby 2.7 would be acceptable, I’m not sure.&lt;/p&gt;

&lt;h3 id=&quot;the--operator&quot;&gt;The … operator&lt;/h3&gt;

&lt;p&gt;The new &lt;code class=&quot;highlighter-rouge&quot;&gt;...&lt;/code&gt; operator of Ruby 2.7 passes all arguments (positional, keywords, block):&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This works in Ruby 2.7 and 3+, but it’s a &lt;code class=&quot;highlighter-rouge&quot;&gt;SyntaxError&lt;/code&gt; in 2.6!
So we’d need something like this if we still want to run on 2.6:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;all_args&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RUBY_VERSION&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;2.7&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;*args, &amp;amp;block&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;...&quot;&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;class_eval&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;RUBY&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
def delegate(&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;all_args&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;)
  target(&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;all_args&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;)
end
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;RUBY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;...&lt;/code&gt; currently does not allow any sibling argument (e.g., &lt;code class=&quot;highlighter-rouge&quot;&gt;def method_missing(meth, ...)&lt;/code&gt;), which makes it unusable in those situations. Probably we should make it work in those situations too, otherwise it’s quite limited.
That is something that seems worth arguing for on &lt;a href=&quot;https://bugs.ruby-lang.org/issues/16253&quot;&gt;this ticket&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;pass_keywords&quot;&gt;pass_keywords&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;pass_keywords&lt;/code&gt; is a simple mechanism that basically simulates &lt;code class=&quot;highlighter-rouge&quot;&gt;...&lt;/code&gt; in a syntax-compatible way:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;pass_keywords&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;respond_to?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:pass_keywords&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RUBY_VERSION&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;3&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;pass_keywords&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It works by forwarding keyword arguments passed to &lt;code class=&quot;highlighter-rouge&quot;&gt;delegate&lt;/code&gt; as keyword arguments to &lt;code class=&quot;highlighter-rouge&quot;&gt;target&lt;/code&gt;.
That only applies for call sites using &lt;code class=&quot;highlighter-rouge&quot;&gt;*args&lt;/code&gt; in &lt;code class=&quot;highlighter-rouge&quot;&gt;pass_keywords&lt;/code&gt; methods, so it’s simple to understand.&lt;/p&gt;

&lt;p&gt;We could also easily make it work for blocks inside that method (the same goes for &lt;code class=&quot;highlighter-rouge&quot;&gt;...&lt;/code&gt;).
That’s actually more useful than it sounds, and allows “saving” a delegated call and trigger it later:&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;pass_keywords&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@call_later&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@call_later&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;pass_keywords&lt;/code&gt; is enough to make the delegation in &lt;code class=&quot;highlighter-rouge&quot;&gt;ActionDispatch::MiddlewareStack::Middleware&lt;/code&gt;
(a rather complicated case) work and requires &lt;a href=&quot;https://github.com/eregon/rails/commit/8b0625ed68&quot;&gt;rather few changes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We could also keep &lt;code class=&quot;highlighter-rouge&quot;&gt;pass_keywords&lt;/code&gt; in Ruby 3+, although I think it would be preferable to use more idiomatic ways for delegation in Ruby 3 and later (either &lt;code class=&quot;highlighter-rouge&quot;&gt;def delegate(*args, **kwargs, &amp;amp;block)&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;def delegate(...)&lt;/code&gt;).&lt;/p&gt;

&lt;h3 id=&quot;ruby2_keywords&quot;&gt;ruby2_keywords&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; is a method introduced in Ruby 2.7 that alters
how a method taking a &lt;code class=&quot;highlighter-rouge&quot;&gt;*rest&lt;/code&gt; parameter behaves. Specifically, if keyword arguments are
passed to such a method, they are remembered by flagging the Hash. When a flagged Hash
is passed at &lt;em&gt;any&lt;/em&gt; call site using a &lt;code class=&quot;highlighter-rouge&quot;&gt;*rest&lt;/code&gt; argument (and no keyword arguments), then
that flagged Hash is converted back to keyword arguments.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ruby2_keywords&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;respond_to?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:ruby2_keywords&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Or use the 'ruby2_keywords' gem:&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'ruby2_keywords'&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;ruby2_keywords&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({})&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [{}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here is how &lt;code class=&quot;highlighter-rouge&quot;&gt;ActionDispatch::MiddlewareStack::Middleware&lt;/code&gt; &lt;a href=&quot;https://github.com/eregon/rails/commit/44a80647f5&quot;&gt;looks like with &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This works fine on Ruby 2.6 (&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; is defined as no-op) and 2.7.
However, what should happen for Ruby 3?
Should we even have a method called &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; in Ruby 3?&lt;/p&gt;

&lt;p&gt;The current idea seems to keep &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; until at least Ruby 2 is end-of-life,
let’s say in Ruby 3.X.
That means however code using &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; will break at that date, unless
the code also includes a version check and uses the Ruby 3-style delegation:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'ruby2_keywords'&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RUBY_VERSION&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;3&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;ruby2_keywords&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is becoming not so pretty, but I think is a good path forward.
Using this, code will not break in the foreseeable future, and the &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt;
workaround is only used on Ruby 2.7, which is the only version needing such a complicated workaround.
Also, we use the Ruby 3-style delegation in Ruby 3 which is rather natural, and not some workaround with complicated semantics.
The Ruby 2 branch can be removed as soon as Ruby 2 support is dropped for that code.&lt;/p&gt;

&lt;p&gt;However, it seems most MRI committers think it’s fine to migrate your code to the version just using
&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; and then ask you again when Ruby 3.X comes out to change the code again to use Ruby 3-style delegation, or &lt;code class=&quot;highlighter-rouge&quot;&gt;...&lt;/code&gt;.
I’m not comfortable with the idea of telling people to change their code, &lt;strong&gt;knowing it will break in Ruby 3.X&lt;/strong&gt;,
and this is part of the reason why I am writing this blog post.&lt;/p&gt;

&lt;p&gt;I don’t think keeping &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; forever is any good either, I think it doesn’t make sense to have a method named &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; in e.g., Ruby 4. We could maybe rename it, but I have other concerns.&lt;/p&gt;

&lt;p&gt;I dislike &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; because it introduces hidden state (a hidden flag on a &lt;code class=&quot;highlighter-rouge&quot;&gt;Hash&lt;/code&gt;).
Do you know which other construct uses hidden state in Ruby? The flip-flop operator is an example.
Hidden state is almost always a sign of bad design for programming languages, it always comes back and bites you by having surprising semantics and non-trivial performance implications.
Practically, it means that &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; might flag a &lt;code class=&quot;highlighter-rouge&quot;&gt;Hash&lt;/code&gt; and when it’s used in a different file at a call site very far away it could magically be passed as keyword arguments even though it’s syntactically passed as a positional argument (e.g., &lt;code class=&quot;highlighter-rouge&quot;&gt;foo(*args)&lt;/code&gt;). Have fun debugging that.&lt;/p&gt;

&lt;p&gt;Also, it slows down every single call site using a &lt;code class=&quot;highlighter-rouge&quot;&gt;*rest&lt;/code&gt; argument, not just the one in the &lt;code class=&quot;highlighter-rouge&quot;&gt;delegate&lt;/code&gt; method.
Because of that I consider it a hack, which seems acceptable if only used in Ruby 2.7, but seems like we’d shoot ourselves in the foot to keep it any longer, both for making migration more difficult when removing it, and for suffering lower performance for all &lt;code class=&quot;highlighter-rouge&quot;&gt;foo(*args)&lt;/code&gt; calls until then.
&lt;a href=&quot;https://bugs.ruby-lang.org/issues/16188&quot;&gt;This issue&lt;/a&gt; has more details about performance implications of &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; and Ruby 3 keyword arguments.&lt;/p&gt;

&lt;p&gt;Finally, &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; introduces magic conversion of positional arguments to keyword arguments.
Is it not ironic that we work on separating positional and keyword argument but then introduce &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; which brings back automatic conversation, i.e., potentially breaks the separation?&lt;/p&gt;

&lt;p&gt;So let’s keep thinking.&lt;/p&gt;

&lt;h3 id=&quot;ruby2_keywords--send_keyword_hash&quot;&gt;ruby2_keywords + send_keyword_hash&lt;/h3&gt;

&lt;p&gt;This is a variant of &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; above but it makes it explicit which call site can convert a flagged Hash to keyword arguments. &lt;code class=&quot;highlighter-rouge&quot;&gt;send_keyword_hash&lt;/code&gt; is then just &lt;code class=&quot;highlighter-rouge&quot;&gt;send&lt;/code&gt; with the additional conversion of a flagged Hash to keyword arguments.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'ruby2_keywords'&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Could be defined by the ruby2_keywords gem too&lt;/span&gt;
&lt;span class=&quot;kp&quot;&gt;alias_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:send_keyword_hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:__send__&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;respond_to?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:send_keyword_hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RUBY_VERSION&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;3&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;ruby2_keywords&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;send_keyword_hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delegate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This still has hidden state by flagging a keyword Hash passed to a &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; method,
but at least it’s explicit where the conversion can occur and where the flag has any effect.
By making it explicit where such a conversion can happen with &lt;code class=&quot;highlighter-rouge&quot;&gt;send_keyword_hash&lt;/code&gt;, it’s both clearer for semantics (easier to debug) and it removes the performance concern by letting other call sites using a &lt;code class=&quot;highlighter-rouge&quot;&gt;*rest&lt;/code&gt; argument behave unchanged.
Also, with &lt;code class=&quot;highlighter-rouge&quot;&gt;send_keyword_hash&lt;/code&gt;, there is no more magic conversion of positional to keyword arguments, so the separation remains clean with no backdoor.&lt;/p&gt;

&lt;p&gt;Here is how &lt;code class=&quot;highlighter-rouge&quot;&gt;ActionDispatch::MiddlewareStack::Middleware&lt;/code&gt; &lt;a href=&quot;https://github.com/eregon/rails/commit/b5fd497a58&quot;&gt;looks like with &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords + send_keyword_hash&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;what-should-we-do&quot;&gt;What Should We Do?&lt;/h2&gt;

&lt;p&gt;Here are a few questions for the Ruby community and for the reader:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Do you like any of these approaches?&lt;/li&gt;
  &lt;li&gt;Is there a better approach that would work but wasn’t considered?&lt;/li&gt;
  &lt;li&gt;Do you think it’s OK to break code/having to change code &lt;em&gt;again&lt;/em&gt; in Ruby 3.X by just recommending to use &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; with no version check? It seems to be what MRI committers are thinking currently.&lt;/li&gt;
  &lt;li&gt;Until when should we keep &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt; (if at all)?&lt;/li&gt;
  &lt;li&gt;What do you think of the hidden state, magic conversion at any &lt;code class=&quot;highlighter-rouge&quot;&gt;foo(*args)&lt;/code&gt; call site and hard-to-debug concerns about &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby2_keywords&lt;/code&gt;?&lt;/li&gt;
  &lt;li&gt;What delegation style would you use once Ruby 2 support is dropped? &lt;code class=&quot;highlighter-rouge&quot;&gt;def delegate(*args, **kwargs, &amp;amp;block)&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;def delegate(...)&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;pass_keywords&lt;/code&gt;?&lt;/li&gt;
&lt;/ul&gt;</content><author><name>Benoit Daloze</name></author><summary type="html">Ruby 3.0 will introduce the separation between positional and keyword arguments. The upcoming Ruby 2.7 release aims to introduce warnings for every argument behavior that will change in Ruby 3.0 to ease migration. However, delegation that works for Ruby 2.6, 2.7 and 3 seems a hard problem to solve.</summary></entry><entry><title type="html">A New Expectation Syntax for ruby/spec</title><link href="https://eregon.me/blog/2019/10/07/a-new-should-syntax.html" rel="alternate" type="text/html" title="A New Expectation Syntax for ruby/spec" /><published>2019-10-07T00:00:00+02:00</published><updated>2019-10-07T00:00:00+02:00</updated><id>https://eregon.me/blog/2019/10/07/a-new-should-syntax</id><content type="html" xml:base="https://eregon.me/blog/2019/10/07/a-new-should-syntax.html">&lt;p&gt;&lt;a href=&quot;https://github.com/ruby/spec&quot;&gt;ruby/spec&lt;/a&gt; is a test suite for the behavior of the Ruby programming language.
The utility to run the test suite is called &lt;a href=&quot;https://github.com/ruby/mspec&quot;&gt;MSpec&lt;/a&gt; and is very similar to RSpec 2.&lt;/p&gt;

&lt;p&gt;The reason to have its own runner and not simply using RSpec is that MSpec is significantly simpler than RSpec 2.
For example, it does not &lt;code class=&quot;highlighter-rouge&quot;&gt;require&lt;/code&gt; any standard library.
Early-stage Ruby implementations typically do not support the entire standard library, but based on the simplicity of MSpec they are still able to run language and core library specs with minimal efforts.&lt;/p&gt;

&lt;p&gt;MSpec also provides a few features that RSpec does or did not have such as
automatically tagging/untagging failing specs, various guards useful for ruby/spec, etc.&lt;/p&gt;

&lt;h2 id=&quot;existing-syntax-in-rspec-2-and-rspec-3&quot;&gt;Existing Syntax in RSpec 2 and RSpec 3&lt;/h2&gt;

&lt;p&gt;MSpec uses a syntax very similar to RSpec 2:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;describe&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;String#start_with?&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;returns true only if beginning match&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;hello&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start_with?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'hel'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This contrasts to the RSpec 3 style:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;RSpec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;describe&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;String#start_with?&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;returns true only if beginning match&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;hello&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start_with?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'hel'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eq&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I don’t particularly like the RSpec 3 style:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;It’s more verbose and needs two method calls instead of one (not counting the matcher)&lt;/li&gt;
  &lt;li&gt;It needs extra parentheses which add visual clutter&lt;/li&gt;
  &lt;li&gt;It’s harder to switch between regular code and specs because some sort of manual code translation is always required (i.e., it’s not just adding &lt;code class=&quot;highlighter-rouge&quot;&gt;.should&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These reasons are why I am mostly happy with the existing RSpec 2 style in ruby/spec.&lt;/p&gt;

&lt;p&gt;Using &lt;code class=&quot;highlighter-rouge&quot;&gt;expect&lt;/code&gt; has of course advantages too, notably when using delegation,
as explained in the &lt;a href=&quot;https://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/&quot;&gt;original blog post about &lt;code class=&quot;highlighter-rouge&quot;&gt;expect&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-various-equality-methods&quot;&gt;The Various Equality Methods&lt;/h2&gt;

&lt;p&gt;One aspect discussed in that blog post is that &lt;code class=&quot;highlighter-rouge&quot;&gt;should ==&lt;/code&gt; generates Ruby warnings.
This used to indeed be annoying.
However in Ruby 2.4+ it’s easy to automatically filter out these warnings by overriding &lt;code class=&quot;highlighter-rouge&quot;&gt;Warning.warn&lt;/code&gt;, which MSpec does.&lt;/p&gt;

&lt;p&gt;When I compare &lt;code class=&quot;highlighter-rouge&quot;&gt;actual.should eq(expected)&lt;/code&gt; to &lt;code class=&quot;highlighter-rouge&quot;&gt;actual.should == expected&lt;/code&gt;, I strongly prefer the latter, because it is much closer to normal Ruby code which would just test &lt;code class=&quot;highlighter-rouge&quot;&gt;actual == expected&lt;/code&gt;.
There are also various equality methods in Ruby: &lt;code class=&quot;highlighter-rouge&quot;&gt;==&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;eql?&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;equal?&lt;/code&gt;.
I’d like to not have to remember any kind of mapping between regular code and specs; I’d like to use the exact same method names in specs.
Continuing on that idea, I’m not such a huge fan of having so many matchers in MSpec to do very basic tests I could do more simply in Ruby code. Here are a few matchers from MSpec:
&lt;code class=&quot;highlighter-rouge&quot;&gt;==&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;eql&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;equal&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;=~&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;=&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;include&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;be_true&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;be_false&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;be_empty&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;be_nan&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;be_an_instance_of&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;be_kind_of&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;be_ancestor_of&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;respond_to&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;have_instance_method&lt;/code&gt;,  …&lt;/p&gt;

&lt;p&gt;So many already, and there are many more!
And of course, there will always be some missing matchers, for instance there is no matcher for &lt;code class=&quot;highlighter-rouge&quot;&gt;String#start_with?&lt;/code&gt;.
Contrary to RSpec, there are no magic &lt;code class=&quot;highlighter-rouge&quot;&gt;be_.../have_...&lt;/code&gt; matchers in MSpec.&lt;/p&gt;

&lt;h2 id=&quot;the-new-simple-and-consistent-syntax&quot;&gt;The New Simple and Consistent Syntax&lt;/h2&gt;

&lt;p&gt;Could we do without having to remember all these mappings and just use normal predicates in Ruby code? That’s what I wanted to try with this new syntax in MSpec:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;describe&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;String#start_with?&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;returns true only if beginning match&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;hello&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start_with?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'hel'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The idea is: add &lt;code class=&quot;highlighter-rouge&quot;&gt;.should&lt;/code&gt; just before the predicate you expect to be truthy, and done!
And this works for any method, because we can implement this through &lt;code class=&quot;highlighter-rouge&quot;&gt;#method_missing&lt;/code&gt;.
This syntax is not new, it was actually already used for &lt;code class=&quot;highlighter-rouge&quot;&gt;should ==&lt;/code&gt;, but now it can be used for any predicate:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should_not&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;
&lt;span class=&quot;s2&quot;&gt;&quot;abc&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=~&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/b/&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;eql?&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;4.0&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;equal?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;s2&quot;&gt;&quot;end&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;end_with?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;d&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;empty?&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;include?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exist?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;README.txt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;should&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;all?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;odd?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I think some of these look really nice, some do not read as nicely, but overall they are all consistent and extremely simple to learn.&lt;/p&gt;

&lt;p&gt;The simplicity of removing &lt;code class=&quot;highlighter-rouge&quot;&gt;.should&lt;/code&gt; to go from specs to regular code is very useful for debugging purposes. Having a minimal amount of self-contained code to reproduce an issue is often key to understand and fix a bug.&lt;/p&gt;

&lt;p&gt;A significant advantage of this approach is that all matchers have a similar output for errors, and provide a lot of information if they fail.&lt;/p&gt;

&lt;p&gt;With the old syntax (&lt;code class=&quot;highlighter-rouge&quot;&gt;s.start_with?('b').should == true&lt;/code&gt;), the error wouldn’t be very helpful and one would need to look at the test code to get an idea of what went wrong:&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;String#start_with? returns true only if beginning match FAILED
Expected false to equal true
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With the new syntax (&lt;code class=&quot;highlighter-rouge&quot;&gt;s.should.start_with?('b')&lt;/code&gt;), we get to see the receiver and arguments, no matter what predicate is used, and it’s immediately clear which expectation failed:&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;String#start_with? returns true only if beginning match FAILED
Expected &quot;a&quot;.start_with? &quot;b&quot;
to be truthy but was false
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And it’s all consistent, so once we learn the formatting of just one error, we know it for all other errors.&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Expected 1 == 2
to be truthy but was false

Expected [1, 3].include? 2
to be truthy but was false
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Previously, the message and formatting would differ for every matcher:&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Expected 1 to equal 2

Expected [1, 3] to include 2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I no longer need to reverse map &lt;code class=&quot;highlighter-rouge&quot;&gt;equal&lt;/code&gt; to &lt;code class=&quot;highlighter-rouge&quot;&gt;==&lt;/code&gt; (how confusing!) and &lt;code class=&quot;highlighter-rouge&quot;&gt;include&lt;/code&gt; to &lt;code class=&quot;highlighter-rouge&quot;&gt;include?&lt;/code&gt;, it just shows me the method which was called and returned something unexpected.&lt;/p&gt;

&lt;h2 id=&quot;expected-actual-to-equal-expected&quot;&gt;Expected Actual to Equal Expected&lt;/h2&gt;

&lt;p&gt;Moreover &lt;code class=&quot;highlighter-rouge&quot;&gt;actual&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;expected&lt;/code&gt; are often unclear in many test framework outputs, such as &lt;code class=&quot;highlighter-rouge&quot;&gt;Expected ACTUAL to equal EXPECTED&lt;/code&gt; above.
Some test-unit frameworks even disagree on which should be first for &lt;code class=&quot;highlighter-rouge&quot;&gt;assert_equal&lt;/code&gt;.
We no longer need a notion of &lt;code class=&quot;highlighter-rouge&quot;&gt;actual&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;expected&lt;/code&gt; values in the new syntax, we always expect a whole predicate to be truthy or falsy, and that’s it. There is no question of order, it’s plain Ruby code and we can see &lt;code class=&quot;highlighter-rouge&quot;&gt;==&lt;/code&gt; was called on the left receiver value, with the right value(s) as the argument(s).&lt;/p&gt;

&lt;p&gt;Of course, this new syntax is not the answer to everything.
For instance, it does not address the fact we need a different syntax for exceptions (and mocks).
We could extend the new syntax to deal with exceptions as going through &lt;code class=&quot;highlighter-rouge&quot;&gt;method_missing&lt;/code&gt; gives a lot of control, but I did not find a nice syntax for that yet.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;The existing RSpec 2-like syntax will still remain available in MSpec,
for compatibility and because migrating ruby/spec would be a very large amount of work.
I also don’t want to force anyone to use the new syntax.
I started using the new syntax in a few specs where I found it to be a clear gain,
&lt;a href=&quot;https://github.com/ruby/spec/commit/a3c52de2cf5858ae452feddb4fd77c9f69e2f6c8&quot;&gt;such as for &lt;code class=&quot;highlighter-rouge&quot;&gt;start_with?&lt;/code&gt; specs&lt;/a&gt;.
The most frequent matcher, &lt;code class=&quot;highlighter-rouge&quot;&gt;should ==&lt;/code&gt;, already uses the new error output,
because that matcher uses the same syntax in the new syntax and
therefore the new implementation is used.
As a fun fact, the implementation of the new syntax actually
&lt;a href=&quot;https://github.com/ruby/mspec/commit/bc841cc0a4&quot;&gt;removes more lines than it adds&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this blog post I presented a new expectation syntax for ruby/spec.
The main advantages are:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Consistent and simple (“add &lt;code class=&quot;highlighter-rouge&quot;&gt;.should&lt;/code&gt; before the predicate”) and therefore easy to learn&lt;/li&gt;
  &lt;li&gt;Very similar to regular code, so going from plain Ruby code to specs or back is trivial&lt;/li&gt;
  &lt;li&gt;No extra mapping between predicates and matchers, matchers are just Ruby predicates&lt;/li&gt;
  &lt;li&gt;Clear output when an expectation fails, showing the relevant values that caused the error&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What do you think? Is this new syntax a good or a bad idea?&lt;/p&gt;</content><author><name>Benoit Daloze</name></author><summary type="html">ruby/spec is a test suite for the behavior of the Ruby programming language. The utility to run the test suite is called MSpec and is very similar to RSpec 2.</summary></entry></feed>