Ruby 3.1 ships experimental YJIT and a replacement debugger
Ruby 3.1 adds an opt-in in-process compiler and replaces the old debugger with debug.gem. Block forwarding and assignment-order changes also refine how ordinary Ruby code expresses and evaluates work.
Making Ruby faster and making its execution easier to inspect are related runtime concerns. A useful implementation needs to preserve the program's behavior while offering ways to understand both ordinary failures and generated code. Ruby 3.1.0, released on December 25, ships experimental YJIT and a rewritten debugger alongside smaller language changes.
YJIT arrives inside CRuby and remains disabled by default. The replacement debug gem supplies a different way to pause and inspect application execution, including integration with external frontends. These additions work at different levels. One changes how selected bytecode executes, while the other improves a developer's access to program state.
I appreciate Ruby's readable object model, but readability does not eliminate runtime questions. An implementation improvement is useful when its scope and assumptions can be inspected. This release provides both a compiler experiment with stated limits and a debugger with several explicit entry points.
YJIT is part of the binary but remains opt-in
The YJIT documentation in the release tag describes a compiler built inside CRuby using lazy basic block versioning. Its initial platform scope is macOS and Linux on x86-64. ARM64 remains planned work, so the availability of Ruby 3.1 on another architecture does not imply availability of this compiler there.
The release announcement gives the explicit command-line switch for enabling it. On a supported Ruby build, a script can be launched through that path without changing the script's source.
ruby --yjit application.rb
This is an execution option, not a source-language mode. The compiler must preserve Ruby semantics for the code it handles and integrate with the interpreter for the rest. The fact that it ships in the same release does not make it the default execution strategy.
That distinction is especially useful because some development documentation still describes settings inherited from the earlier project configuration. The release announcement is explicit about the shipped default. Users evaluating the feature need to identify the Ruby build and activation flag involved instead of assuming that the presence of YJIT documentation means it was active.
Specialization follows reached code paths
Lazy basic block versioning compiles code with type context obtained during execution. The compiler can keep specialized versions of a block for different contexts and avoid rechecking facts already known on that path. It need not compile every possible combination before executing useful work.
The earlier YJIT design account explains why fast warmup and application-oriented workloads matter to the project. Building inside CRuby retains compatibility with its runtime while allowing direct control over machine-code generation. The compiler's usefulness depends on how much application execution reaches supported paths and how cheaply those paths become compiled.
The release's documentation identifies the lack of garbage collection for generated code as a limitation. Code that becomes unusable does not yet have its executable memory reclaimed. This is separate from reclaiming ordinary Ruby objects. Generated code has its own lifetime and resource cost, which the runtime needs to account for independently.
The documented statistics and disassembly facilities support investigating that behavior. A compiled method can have generated machine code available for inspection through RubyVM::YJIT.disasm. A method that never compiled has no such code to display. Debugging and statistics build options also affect the evidence available and can add overhead, so their results need to be read with the build configuration in mind.
Debugger entry points match different workflows
The debug 1.4.0 documentation describes the replacement for the traditional lib/debug.rb. A developer can load it explicitly, launch a script with rdbg, or insert a breakpoint through a binding. That gives applications several routes into the same debugging system.
An explicit source breakpoint looks like this. It stops before the code that computes the total, making the local inputs available for inspection.
require "debug"
price = 12
quantity = 3
binding.break
total = price * quantity
puts total
The debugger can show local variables and continue execution after the pause. Alternatively, rdbg can launch a script and set breakpoints without editing its source. These are different ways to choose where inspection begins, useful depending on whether changing the program is convenient.
For commands such as Rails or Rake, the documented -c option selects a command from PATH instead of interpreting the argument as a script filename. The double-hyphen separator keeps the command's own flags distinct from debugger options. With Bundler, the documentation also requires the debug gem in the Gemfile so it belongs to the selected dependency environment.
The performance claim in the debugger README is scoped to execution outside stepping and breakpoints. It should not be expanded into a claim that every active debugging operation is free. Inspecting state and suspending execution are deliberate interventions. The implementation's lower idle cost makes it easier to retain debugging capability without confusing that cost with the work done during inspection.
Remote attachment separates the console from the process
The same debugger supports a target process with a separately attached console. By default, its open mode uses a Unix domain socket with an automatically generated path. An attach command connects another console to that endpoint. This is useful for a process without an interactive terminal.
TCP attachment is also documented, with localhost as the default when a port is supplied. The README states that communication is not encrypted. The endpoint therefore belongs within the operational access controls appropriate to a debugger, which can inspect and evaluate application state. Opening it is a separate choice from starting a normal Ruby application.
External frontend support includes VS Code and Chrome. The debugger can expose the process through an interface those tools understand while keeping Ruby execution in the target process. A familiar graphical frontend does not mean the application has become a browser program. It provides another client for the debugging session.
I regard these entry and attachment choices as a practical improvement. A useful debugger needs to fit the way the application actually starts. A source breakpoint, a command launcher and a separate console address different points in that lifecycle without requiring the application to adopt a different language model.
Anonymous block forwarding removes an unnecessary local name
The Ruby 3.1 NEWS file documents anonymous block forwarding. A method that only passes its received block to another method can use an unnamed block parameter. This expresses that the wrapper forwards the block without treating it as a separately named local value.
def inner
yield 7
end
def outer(&)
inner(&)
end
outer { |value| puts value }
This is a small change to a common delegation pattern. Naming the block remains available when the method needs to refer to it. The new syntax makes a pure forwarding role concise without changing which method eventually yields to the caller's block.
The NEWS file also permits omitted values in hash literals and keyword arguments when the name already identifies the intended value. For example, {price:, quantity:} can stand for explicit pairs using the locals with those names. The shorthand reduces repetition while retaining the keys in the source. It does not perform a runtime search for unrelated variables.
Evaluation order can change observable behavior
Multiple assignment now evaluates the relevant left-hand-side receivers before the right-hand-side expressions, matching the single-assignment order described in NEWS. Earlier versions evaluated the right-hand side first in the documented multiple-assignment case. Code with side effects in receiver expressions can observe that difference.
This is a compatibility point that a broad claim of Ruby 3.0 compatibility should not conceal. Most simple assignments do not depend on competing side effects, but an accessor receiver can perform work before the setter executes. The order of those computations belongs to the language's behavior, independently of whether YJIT is enabled.
The NEWS example separates receiver evaluation from setter invocation. Ruby first obtains the objects that will receive the writes, then computes the values to assign, and finally invokes the setters. That distinction prevents a misleading interpretation in which evaluating the left side means performing the write immediately. Selecting a receiver can itself have side effects before any stored value changes.
For code that obtains a receiver by calling a method, the change can determine which object ultimately receives an assignment. If computing a right-hand-side value also changes state consulted by that method, the old and new orders can choose differently. Such a case needs to be understood as an evaluation-order dependency in the application. Enabling or disabling YJIT does not select the older language rule, because the compiler is responsible for implementing the semantics of the Ruby version containing it.