Ruby 3.3.3 releases parser state that Ripper left allocated
A Ripper cleanup fix addresses growth during repeated parsing of unusual source. The maintenance release also adjusts Prism inspection commands and updates distributed gems.
A parser can finish processing an input and still retain native memory that its next invocation does not need. Repeating the operation then accumulates memory even if application code discards every parse result. Ruby 3.3.3 corrects such a leak in Ripper, making a maintenance release relevant to programs that inspect Ruby source repeatedly.
The June 12 announcement lists the Ripper parsing leak alongside YJIT and serialization fixes. Its underlying report, Bug #20521, demonstrates growing resident memory while repeatedly parsing source containing a heredoc and a word-array expression. The reporter observes the leak in Ruby 3.3.0 and says the same example does not leak in 3.2.2.
I find Ruby's blocks and collection methods readable, but source-processing tools encounter much less ordinary code than an application typically executes. A parser must release its temporary state even for unusual or incomplete syntax. The result's lifetime and the parser's internal allocations need separate accounting.
The missing cleanup is in the parser destructor
The correction adds cleanup of p->lex.strterm in rb_ruby_parser_free. It frees that lexer state and clears the pointer before freeing the parser object. The source change is small because the missing responsibility belongs at one cleanup point.
This explains why discarding the Ruby-level parse result is insufficient. The leaked allocation belongs to the parser's native implementation. Releasing the outer parser object without releasing the subsidiary state loses the opportunity to reclaim that memory through the intended cleanup path.
The report's loop repeatedly calls Ripper.sexp_raw with the same source. Its purpose is to isolate accumulation from changing application data. A fixed input producing continued memory growth is useful evidence that the repeated operation retains something it should have released.
The example's reported memory readings are evidence for that reproducer, not a universal rate for all Ripper users. Different input and process behavior can produce different observations. The relevant mechanism is the omitted lexer-state cleanup, rather than a claim that every parse leaks a fixed number of bytes.
The regression exercises memory across many parses
The patch adds a memory-leak test using an invalid heredoc inside block parameters. It invokes Ripper.new(source).parse repeatedly and uses the test suite's resident-memory checking facility. That verifies a property a normal parse-tree equality assertion would not capture.
A parser can return the expected representation and still leak. A regression test that only checks returned tokens would therefore miss the failure this patch addresses. The existing tokenization assertion remains useful for source preservation, while the new memory test covers cleanup under repetition.
The chosen source also illustrates why syntax tools need error-path behavior. An editor or analyzer may receive source midway through a change. The parser's responsibility to clean up does not depend on the input being a finished program that an application would deploy.
The backport record marks the fix as needed for Ruby 3.3 and not needed for 3.1 or 3.2. That scope should be retained. The report does not establish a shared leak in every maintained Ruby series merely because those versions all provide Ripper.
The destructor is also a more appropriate correction point than asking callers to add cleanup after each parse. Callers do not own the lexer's internal allocation. Keeping its release with parser destruction makes that ownership explicit in the implementation and applies the correction to both the convenience API in the report and the direct parser construction used by the regression test.
Parser inspection gets a clearer command structure
The release also replaces --dump=prism_parsetree with the combination --parser=prism --dump=parsetree. Selecting the parser and choosing the diagnostic output are separate options. That makes the command express both decisions explicitly.
ruby --parser=prism --dump=parsetree example.rb
This is a parse-tree inspection command for the supplied file. The option change does not mean Prism becomes the default execution parser in Ruby 3.3.3. The announcement specifically describes replacing one inspection option with another combination.
Another parsing-related change makes invalid-encoding symbols raise SyntaxError instead of EncodingError. That changes the exception classification tools may observe. The release also names a separate correction for Ripper.tokenize, so the memory fix should not be treated as the explanation for every parser-related entry.
The distribution includes library maintenance too
Ruby 3.3.3 includes RubyGems 3.5.11 and Bundler 2.5.11. Distributed library updates include REXML 3.2.8 and strscan 3.0.9. The announcement also lists fixes around RubyVM::InstructionSequence#to_binary, keyword splatting and build environments.
Those changes give the patch a broader scope without requiring an application to adopt a new language version. My interest in the Ripper correction is the separation between successful parsing and complete cleanup. A long-lived source-processing tool needs both, and this release restores the missing cleanup for the lexer state identified by the regression.