Ruby 4.0 preview2 separates numbered parameters from local variables
Ruby 4.0 preview2 changes Binding access to numbered parameters and updates Unicode data. Its smaller compatibility changes clarify where dynamic method behavior and language-level rules diverge.
A debugger's list of local variables can become a language-semantics problem when implicit block parameters appear in it. Ruby's numbered parameters have lexical rules that ordinary locals do not fully describe. The old Binding interface could expose an outer block's numbered parameter inside an inner block and even permit changes through a local-variable setter.
Ruby 4.0.0-preview2, released on November 17, removes numbered parameters from Binding#local_variables. Its local-variable getter and setter reject them as well. The release also updates Unicode and Emoji data and changes the treatment of splatting nil. These are specific behavior changes within a preview, not a declaration that every dynamic facility now follows a new model.
I find Ruby's consistent object interfaces useful, but reflective APIs need precise limits. A method that lists locals should not silently turn every implicit language binding into an ordinary mutable local. This change makes that distinction visible to the tools that inspect Ruby execution.
Numbered parameters exposed a scope mismatch
Yusuke Endoh's issue #21049 demonstrates the problem with nested blocks. An outer block uses _1. An inner block then asks its Binding for local variables or reads _1 through the local-variable interface. The report says the old behavior exposes the outer parameter where it should not be available through that API.
The following follows the structure of that diagnostic example. The nested block does not explicitly refer to a numbered parameter, but asks its Binding to find one by name:
"outer".tap do
_1
"inner".tap do
p binding.local_variables
p binding.local_variable_get(:_1)
end
end
The reported old behavior included :_1 and returned the outer value through the getter. The preview's local-variable interface instead excludes numbered parameters and rejects that access. It does not remove numbered parameters from ordinary block expressions.
The setter presents another mismatch. The issue shows local_variable_set changing the value associated with _1. That treats an implicit parameter as though it were a programmer-declared local. The correction refuses to extend the local-variable API in that way.
The motivating discussion also considers the implicit it parameter and a real local variable with the same spelling. That ambiguity strengthens the argument for distinguishing implicit parameters from local variables in reflection. The release announcement's concrete compatibility item concerns numbered parameters, so it should be read at that stated scope.
Reflection consumers see a changed contract
A tool that assumed every block parameter appeared in local_variables can observe fewer names. A tool that fetched a numbered parameter using local_variable_get can receive an error instead of its value. Those are API behavior changes even though ordinary code using _1 can remain unchanged.
The issue records a debugger developer asking about this loss of access before the preview. That is a real consumer of the behavior, rather than a purely theoretical concern. A debugger needs to display implicit arguments, but that requirement does not establish that the ordinary local-variable API is the right interface.
Endoh's initial discussion proposes possible dedicated APIs but does not treat them as necessary merely to correct the existing behavior. Matz accepts the expected outcomes in the report and asks to wait for concrete use cases before adding additional interfaces. The correction and any replacement inspection facility therefore need separate evaluation.
This is a useful boundary in an extensible language. Introspection exposes runtime structure to other software, so accidental behavior can acquire consumers. Correcting the structure can still require those consumers to adapt. That explains why a small core-class note can matter to debugging tools more than to an application using the blocks being debugged.
Splatting nil no longer invokes its conversion method
The preview also includes feature #21047, proposed by Jeremy Evans. Historically, *nil called nil.to_a. The corresponding keyword splat, **nil, did not call nil.to_hash. The change makes the positional case follow the same special handling of absence.
For an ordinary call, the visible argument sequence remains empty:
def arguments(*values)
values
end
p arguments(*nil)
The changed contract matters if code redefines nil.to_a or depends on observing that call. The issue describes that as a minor compatibility break. It is the method invocation that disappears, not the meaning that an ordinary nil splat contributes no positional values.
The implementation can also avoid a temporary allocation for some call forms. The issue explains that a nonmutating splat instruction can use a shared frozen empty array. An array literal such as [*nil] still needs its own array result, so the optimization does not mean every appearance of the syntax becomes allocation-free.
The issue also describes concatenation with a nil splat. A form such as [1, *nil] can retain the first array without constructing another array for the absent contribution. The implementation therefore distinguishes the result container a program actually needs from a temporary conversion container used only to express an empty argument sequence. That distinction explains the optimization without claiming that Ruby stops allocating arrays for ordinary collection literals.
I prefer this kind of narrowly specified consistency change to a vague promise that dispatch becomes faster. The source identifies which conversion call disappears and which allocations can be avoided. It also names the unusual metaprogramming behavior that changes as a consequence.
Unicode data changes independently of reflection
The release updates Unicode to 17.0.0 and Emoji to 17.0, applying the update to String and Regexp. This is a data-version change with implications for text handling. It should not be reduced to a claim that Ruby merely recognizes a few additional display symbols.
The announcement links the Unicode update issue, which records preparation beginning while the upstream data was still in development. The preview is a point at which applications can examine the runtime's published behavior with the updated data. It does not determine what fonts or rendering support a separate user interface provides.
The same preview keeps ZJIT explicitly experimental and says it is not ready to accelerate most benchmarks. That warning prevents treating a major version label as performance evidence. The concrete changes available for examination include the local-variable reflection contract and the nil-splat conversion rule, each with a specific before-and-after behavior.