February 18, 2022·4 min read·engineeringrubyyjitruntime

Ruby 3.1.1 fixes YJIT keyword argument placement

Ruby's first 3.1 maintenance release repairs an argument-handling bug in YJIT. The regression test reduces a Rails caching symptom to optional positional arguments and keyword ordering.

A cache can store the wrong output even when its own storage operations work correctly. Ruby's YJIT argument bug provides an example. The compiled call could pass values to the wrong parameter positions, and the resulting Rails collection-rendering failure appeared as repeated cached content. The useful diagnosis was below the cache API, in the machinery that prepared a method call.

The February 18 Ruby 3.1.1 announcement lists this correction among the first maintenance fixes for the stable 3.1 series. Its description identifies the combination of keyword and default arguments. The fix is a correctness change to the runtime's treatment of ordinary Ruby calls, rather than a new convention for application authors to adopt.

I find Ruby's readable method calls one of its strengths. That readability depends on implementation details preserving the language's argument rules. A caller using a keyword name should not need to know how a compiled call rearranges its stack entries to reach the intended parameter.

The small test explains the large symptom

The YJIT patch adds regression tests for a method with an optional positional parameter followed by two keyword parameters. Its explanation says that the implementation used the wrong stack-pointer index when rearranging keyword arguments that were not in the order expected by the receiver.

An adapted version of the test makes the intended result explicit.

def arguments(value = {}, left: nil, right: nil)
  [value, left, right]
end

p 5.times.map { arguments(1, right: 2) }.uniq
# [[1, nil, 2]]

p 5.times.map { arguments(right: 1) }.uniq
# [[{}, nil, 1]]

The first call supplies the positional argument and the later keyword. The second relies on the positional default while still supplying that later keyword. Both leave the first keyword at its default value. Repeating the calls and taking unique results checks the returned arrangement, rather than accepting a call merely because it does not crash.

That is an important property of this bug. A runtime can produce a valid Ruby object with incorrect contents. An assertion about the exact values in their exact positions detects that behaviour. A test that checks only successful execution or the general shape of the return value would establish less.

Optional positions changed the offset

The implementation patch changes the count used before processing keyword arguments. Previously, the code used the number of leading required positional arguments. Its corrected count includes both required and optional positional arguments expected before the keyword area.

The swap operation then subtracts that corrected count when locating the entries to exchange. In a method with an optional positional parameter, the earlier calculation could identify the wrong positions. The language-level call remained legitimate, but YJIT's generated rearrangement did not preserve the receiver's expected argument layout.

This explains why the test needs more than a generic method call with a keyword. Optional positional parameters participate in the layout calculation. The regression cases exercise both a supplied positional value and a defaulted one, keeping that interaction visible in a short program.

The correction also shows why a framework-level failure can require a runtime fix. Application code can use the documented calling convention and still receive incorrect behaviour from one execution implementation. The patch repairs the convention's implementation instead of requiring framework callers to avoid a valid parameter combination.

The report connects argument values to cached output

Bug 18453 describes a minimal Rails application whose collection rendering behaved differently with YJIT enabled. The investigation traced the problem to incorrect arguments reaching cache_fragment_name. That observation connected the visible repeated output to the method-call machinery addressed by the patch.

The report is someone else's reproducer, not evidence of a failure in every Rails application. It nevertheless supplies a useful chain of reasoning. A framework operation receives incorrect inputs, those inputs affect caching behaviour, and a reduced argument test isolates the runtime error. The release announcement then identifies the correction as part of Ruby 3.1.1.

I value the reduced test because it makes the defect understandable without a complete deployment. Ruby's dynamic calling model already requires the implementation to manage substantial runtime information. A small test that records the intended parameter values gives that machinery a concrete correctness condition.

The maintenance release includes other runtime and library fixes, including crashes and a package-tool update. Those changes have their own causes. For this YJIT defect, the decisive detail is the positional count used while arranging keywords, and the patch preserves the expected call results for both explicit and defaulted positional arguments.

The release also incorporates RubyGems 3.3.7 and Bundler 2.3.7, as its announcement records. That is a separate change from the generated-call correction. An interpreter distribution includes supporting package tools, so its patch number alone does not identify which subsystem changed. The linked issue and regression test provide the more precise account of what this particular runtime fix establishes, without suggesting a change to keyword syntax or the meaning of optional arguments.