Ruby 3.4 preview1 exposes assumptions in everyday Ruby code
Ruby 3.4's first preview introduces a string-literal migration warning and implicit block parameters. Changes to iteration and backtraces show where familiar Ruby behavior meets runtime optimization and compatibility.
Changing the meaning of a string literal requires a migration plan for code that already mutates it. Ruby's first 3.4 preview makes that problem concrete. Files that omit a frozen_string_literal comment acquire new behavior, with deprecation warnings available when their literal strings are mutated. The May 16 announcement also introduces it for block parameters and changes several diagnostic interfaces. Together these changes give application authors specific compatibility questions to investigate in a preview release.
I came to Ruby from Perl and still find its object model and collection methods approachable. That preference makes the details of this preview interesting to me. Readability depends on knowing what a short expression means, including whether a literal can change and which value an unnamed block parameter refers to. Ruby 3.4-preview1 changes those expectations in places that ordinary application code encounters.
String literals acquire a migration state
Jean Boussier's proposal in Feature #20205 describes the compatibility problem behind the new warning. The frozen_string_literal: true pragma arrived in Ruby 2.3. Making it the default would break existing mutations, so the proposal separates the introduction of warnings from the eventual change in default behavior. Its example release numbers describe a possible progression, not a committed schedule.
The proposal leaves files with an explicit true or false pragma unchanged. For files without either comment, it describes a putchilledstring instruction that marks literal strings so mutation can trigger a warning. That gives maintainers a way to find affected operations while the operations still work. An explicit false comment remains a way to declare that a file expects mutable literals.
This is also a disagreement about the value of changing a default. Yusuke Endoh asks for evidence that the performance benefit justifies compatibility costs. Other participants argue that explicit mutable buffers improve readability. Those are different arguments, and the proposal's migration mechanism does not settle either one by itself.
The preview's string tests make the intermediate behavior more precise. A chilled string reports that it is frozen, yet the tests also exercise mutation. Its duplicate is mutable, and unary plus returns a separate mutable string. Code that treats frozen? as a complete description of mutation behavior therefore needs particular attention in this preview.
The release documents Warning[:deprecated] = true as a way to enable these diagnostics. For a file without a magic comment, the following small example exercises the warning:
Warning[:deprecated] = true
message = "ready"
message << "!"
The mutation is the operation under investigation. It does not become an unconditional FrozenError merely because this is the 3.4 preview. Where a mutable buffer is intentional, the behavior covered by the tagged tests supports making that intention explicit:
message = +"ready"
message << "!"
That distinction matters for library code that receives a string from somewhere else. A warning can identify a mutation far from the literal's definition. The useful repair depends on whether the receiver owns a buffer or expects a value that callers can share unchanged.
An implicit parameter for short blocks
Takashi Kokubun's Feature #18980 explains it through dissatisfaction with numbered parameters. For a block taking one value, _1 suggests an ordering that the reader may not need. The proposal gives that common case an ordinary word, while retaining named parameters for blocks where the argument's role needs explanation.
[1, 2, 3].map { it * 2 }
[1, 2, 3].map { |number| number * 2 }
The first expression uses the new implicit parameter, while the second names the same role explicitly. Compatibility with existing methods called it also receives attention in the proposal. A call with arguments or a block remains a method call, which explains why the familiar RSpec form with a description and block does not become an implicit parameter reference.
This change does not make every existing occurrence of the word interchangeable. An explicit local variable and an ordinary block parameter affect name resolution. The useful scope for the new spelling is a short block whose input is already obvious. I prefer an explicit parameter when its name explains the operation, especially when a block contains enough work that the reader must remember what the value represents.
Optional keywords can remain absent
The problem behind Bug #20064 comes from assembling optional keyword arguments. Its motivating example conditionally constructs a hash of invitation attributes. If the condition is false, the expression produces nil, which previously made the keyword splat fail. The issue compares that behavior with the accepted use of nil in an array splat.
Preview1 accepts **nil in a method call as no keywords. The release specifies that it does not call conversion methods. This gives a conditional keyword hash a useful absent state without requiring every caller to replace nil with an empty hash.
def configure(**options)
options
end
optional = nil
configure(**optional) # => {}
The change concerns what the call passes. It does not supply missing required keywords or change the receiving method's requirements. For a wrapper that accepts an optional set of settings, the caller can now preserve absence through the splat itself. That removes a special case from conditional argument assembly.
Ruby iteration gives YJIT more code to optimize
Rewriting Array#each in Ruby might sound surprising if C is assumed to be the faster implementation language. Kokubun's Feature #20182 identifies a more specific cost. Calling Ruby code from C is more expensive than a Ruby-to-Ruby call, and the boundary prevents YJIT from optimizing across Ruby instruction sequences. A loop repeats that cost for every element.
The discussion also shows why translating a C loop into ordinary Ruby is insufficient. Checking an array's length and then reading an element as separate operations can allow a concurrent mutation between them. Benoit Daloze points out that this could produce nil where the array never contained it. The eventual approach uses an internal operation to handle fetching the next element.
The tagged implementation of Array#each contains the Ruby loop and calls ary_fetch_next through Primitive.cexpr!. Its body yields the fetched value and returns the array after iteration. Without a block, it returns a sized enumerator. The source also documents modification of the array during iteration, so preserving behavior matters alongside exposing the loop to the optimizer.
This implementation is not a recipe to paste into an application. Primitive is part of Ruby's own implementation machinery. The relevant architectural decision is that Ruby can express the control flow while an internal operation preserves the element-fetch behavior. The language of the outer loop alone does not determine how much work the JIT can optimize.
Warnings distinguish performance costs from ignored work
Jean Boussier's Feature #20429 addresses a consequence of redefining core methods. The interpreter and JITs give special treatment to operations such as integer arithmetic and some string methods. Replacing those methods can disable optimized handling, including when a dependency performs the replacement.
Preview1 adds a performance warning category for this case. Enabling that category makes these redefinitions visible when Ruby encounters them:
Warning[:performance] = true
The warning reports a runtime optimization consequence of a legal Ruby operation. It does not make redefining the method a syntax error. That distinction is useful for dependency diagnosis because an application can remain functionally correct while losing an optimization through code loaded elsewhere. I value this diagnostic because Ruby's flexibility otherwise leaves the performance effect of such a replacement difficult to see from the calling code.
An ignored block presents a different problem. Koichi Sasada's Feature #15554 starts with a wrapper around open that fails to forward its caller's block. The caller expects the wrapper to execute work with an opened file, but the method ignores the block. Ordinary argument checking does not explain that missing execution.
The preview emits a warning in verbose mode when a method does not use a passed block. Its documented switch is -w, separate from explicitly enabling performance warnings. For wrapper authors, the distinction helps identify whether the method failed to pass work onward or merely changed the runtime's optimization assumptions. These diagnostics concern different defects even when both arise from a small method definition.
Backtraces retain more useful identity
Feature #19117 begins with a backtrace containing method names such as inspect. Jean Boussier argues that a method name alone makes a large backtrace hard to understand. Including its owner gives the reader more information before opening the source location.
Preview1 displays class names before method names when the class has a permanent name. It also replaces the opening backtick in diagnostic quotes with a single quote. The release notes include corresponding changes to Kernel#caller and backtrace-location methods. Code that parses these strings or compares diagnostic output has a compatibility concern even if the underlying exception is unchanged.
There is a separate improvement for code that constructs exceptions. Sylvain Joyeux's Feature #13557 describes the loss of structured location information when a library rewrites a backtrace using strings. Passing location objects previously failed, while passing strings could leave backtrace_locations unavailable on the resulting exception.
The preview lets Exception#set_backtrace accept arrays of Thread::Backtrace::Location. The raising interfaces accept that format as well. A library that filters a backtrace can therefore work with location objects without first reducing them to display text. That matters for code which both presents an exception to a person and exposes its locations to another tool.
The NEWS file pinned to preview1 records the release's changes without mixing in subsequent development. Parser experiments are also possible, but the tagged command-line implementation still treats parse.y as the default and requires --parser=prism to select Prism. Its Prism compiler warning explicitly describes incomplete compatibility. An application testing the new string or diagnostic behavior under the default parser is therefore conducting a different experiment from one selecting Prism, and the preview provides a way to keep those results distinct.