December 25, 2024·6 min read·engineeringrubyprismyjitruntime

Ruby 3.4 adopts Prism and exposes more runtime choices

Prism becomes the execution parser while YJIT gains broader memory accounting and modular GC separates collector implementations. The release also changes how socket connections make progress.

A parser distributed as a library and a parser used to execute every program have different compatibility obligations. Ruby 3.4 makes Prism the default execution parser, moving it into the ordinary path for running Ruby source. The release also makes more runtime behavior explicit, including JIT memory accounting and selection of garbage-collector implementations.

The December 25 announcement presents Prism as an internal improvement with little expected user-visible change. The conventional parser remains available through --parser=parse.y. That fallback is a concrete way to distinguish a parser compatibility issue from a general application failure while both implementations are available.

I still value Ruby's readable object model and its blocks, even though I am cautious about making it a default choice for new systems. This release interests me because it changes substantial implementation choices without asking ordinary application methods to adopt a different programming model.

Prism has two roles in the ecosystem

The Ruby 3.3 announcement introduced Prism as a default gem and described it as a portable, error-tolerant recursive-descent parser. It could support tools that inspect source, with APIs returning parse results and comments. Executing a program through its compiler was then an experimental option.

Those are separate uses of parsing. An editor needs a useful representation of source that may contain errors because someone is still typing. The interpreter needs a parser and compiler path that preserves the language behavior expected by existing programs. Using the same parsing project can reduce duplication, but the execution role makes compatibility especially consequential.

Ruby 3.4 now chooses that path by default. This does not mean Prism first becomes available today. The change concerns which parser processes a normal invocation of the interpreter, without a flag selecting the alternative.

The release asks users to report compatibility issues and preserves the old parser selector. It does not propose a new Ruby syntax dialect that applications must translate into before running. The intended behavior is the existing language, with a different parser implementation supplying its internal representation.

A short block can name its one input implicitly

The new it reference supplies a name for a block's first parameter without an explicit parameter declaration. The announcement relates it to _1, with a narrower reading cue for a simple block whose intention is to use one input.

names = ["alice", "bob"]
uppercase = names.map { it.upcase }

This is a Ruby 3.4 expression using the new reference. The call still maps a block over the collection. The syntax changes how that block refers to its input, leaving the operation and result readable without introducing a local parameter name solely for one method call.

An explicit parameter remains useful when its name conveys domain meaning or when the block becomes more involved. The release recommends it for simple cases where the reference explains itself. That is a sensible scope for a shorthand whose value depends on a reader understanding which object receives the method call.

Ruby 3.3 had already deprecated zero-argument it calls in blocks without ordinary parameters in preparation for this change. The earlier announcement provides the compatibility history. The new reference consequently has a transition behind it, rather than appearing without any previous indication that the name's interpretation would change.

YJIT accounts for more than executable memory

The new --yjit-mem-size option sets a unified YJIT memory limit, with a default of 128 MiB. The announcement presents it as a more intuitive alternative to the earlier executable-memory option. Generated code is only part of the compiler's memory use because its metadata also occupies storage.

Compressed context information reduces that metadata cost. YJIT also gains register allocation for local variables and Ruby method arguments. These are implementation changes that affect how it represents and executes a Ruby method, without requiring the method's author to manually manage registers or compiler contexts.

The release adds a compilation log through --yjit-log, with access to its tail from RubyVM::YJIT.log. Runtime statistics also expose more information about invalidation, inlining and metadata encoding. Those facilities help identify what the JIT actually compiles and how it uses its supporting structures.

The announcement reports better performance across most of its benchmarks on x86-64 and Arm64. That is a report about the release's benchmark results. It should not be converted into a universal percentage improvement for an application whose workload those benchmarks do not establish.

Ruby implementations can help the Ruby JIT

When YJIT is enabled, more core collection operations use implementations written in Ruby. The announcement names Array#each, Array#select and Array#map. That is an instructive change because implementing a core method in the interpreter's native language is not the only route to efficient execution.

A JIT can optimize Ruby operations it understands as part of the executed program. Moving an operation into Ruby can therefore provide optimization opportunities within that model. The release associates these rewrites with better performance, without claiming that rewriting arbitrary C code in Ruby will always improve it.

YJIT also inlines small methods, including ones returning a constant, self or an argument. Eliminating a method-call boundary for these cases reduces work while preserving the result the method expresses. Specialized handling of string byte operations and bitwise operations addresses other concrete execution patterns.

I find these changes more persuasive than a broad claim that Ruby's age no longer matters. They identify specific costs and implementation opportunities. The language remains dynamic, while the runtime improves how it executes common operations and reports the memory needed to do so.

Modular GC creates an implementation boundary

Ruby 3.4 can load alternative garbage collectors dynamically when built with --with-modular-gc. At runtime, RUBY_GC_LIBRARY selects the collector library. A normal build does not acquire that capability merely because someone sets the environment variable.

The built-in collector has moved into gc/default/default.c and interacts with the interpreter through the API in gc/gc_impl.h. The announcement describes building that default collector as a library too. This makes the boundary apply to the existing implementation, rather than defining an extension mechanism only a new experimental collector would use.

An experimental MMTk-based library is also provided. Building it requires the Rust toolchain. The release identifies it as experimental, so modularity should not be read as a claim that every available collector is equally established for every workload.

The practical significance is the separation between interpreter behavior and one collector implementation. Researchers can implement against an explicit interface and select a library in a suitably configured build. This creates a route for evaluating different collection strategies without presenting an alternative collector as the new default for all Ruby 3.4 programs.

Socket connections no longer wait through every address serially

The socket library adopts Happy Eyeballs Version 2 for TCPSocket.new, TCPSocket.open and Socket.tcp. The announcement describes concurrent IPv6 and IPv4 resolution, followed by connection attempts that prioritize IPv6 and stagger parallel attempts at 250-millisecond intervals.

The first successful connection wins and the other attempts are canceled. That changes the behavior when one address family or destination is slow or unavailable. A usable alternative no longer has to wait through the same fully serial sequence described for earlier Ruby versions.

This behavior is enabled by default. The release supplies a global environment setting, RUBY_TCP_NO_FAST_FALLBACK=1, and Socket.tcp_fast_fallback=false for disabling it globally through Ruby. The fast_fallback: false keyword provides a per-call choice on the supported interface.

These controls expose a networking policy that can affect observable connection timing. They are separate from parser choice or JIT activation, even though all arrive in the same interpreter release. Applications dealing with dual-stack destinations receive the new behavior through their socket operations.

Compatibility includes diagnostics and library parsing

Ruby 3.4 also changes backtrace presentation to include a permanent class name before a method name and uses ordinary single quotes in place of the previous opening backtick. Hash#inspect changes its rendering of symbol keys and spacing around other key associations. Programs that treat these display strings as exact data can therefore observe changes even when their primary computation is unchanged.

URI changes its default parser from RFC 2396 compliance to RFC 3986 compliance. That is another parsing change with a different subject from Prism, since it concerns URL interpretation inside a library rather than Ruby source. The release's compatibility notes keep those responsibilities distinct.

For an application adopting Ruby 3.4, the substantive choices now include the execution parser and a broader JIT memory budget. A build configured for modular collection adds another explicit implementation selection. The preserved parser flag and documented runtime controls give those choices concrete interfaces, while the socket library changes connection progress under its new default algorithm.