Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
50745e3
Update bundled gems list as of 2026-08-07
matzbot Aug 7, 2026
85e3df9
GC: per-Ractor local GC — GC engine
ko1 Aug 1, 2026
d549b39
GC: per-Ractor local GC — Ractor (move courier / absorb / teardown)
ko1 Aug 1, 2026
84faabf
GC: per-Ractor local GC — generic fields (single global table)
ko1 Aug 1, 2026
7de3539
GC: per-Ractor local GC — Thread / scheduler
ko1 Aug 1, 2026
65424e1
GC: per-Ractor local GC — VM (lock / roots / write barrier)
ko1 Aug 1, 2026
54e179a
GC: support modular GC (mmtk) — coverage for single-objspace impls
ko1 Aug 1, 2026
5f87918
GC: per-Ractor local GC — tests
ko1 Aug 1, 2026
e93c110
tool/rbs_skip_tests: skip GC/ObjectSpace type tests that break on Rac…
ko1 Aug 1, 2026
78ec25e
Ractor#value: return the value only once
ko1 Aug 3, 2026
09aab78
Ractor: derive object belonging from the owning objspace
ko1 Aug 3, 2026
ff5b7c4
Ractor: close an undelivered IO's fd when freeing a move courier
ko1 Aug 3, 2026
4a13043
thread: restore the atfork reinitialization of vm->once_lock
ko1 Aug 3, 2026
9d3b43f
Ractor: do not leak generic_fields_lock when the insert raises
ko1 Aug 3, 2026
90fceb3
Fix raise-path bugs in Ractor#value and Ractor creation
ko1 Aug 4, 2026
b5da1e4
Ractor: drop the pin list if basket allocation raises
ko1 Aug 4, 2026
5dd63db
NEWS: per-Ractor GC
ko1 Aug 4, 2026
4f6c34e
Ractor: test that a monitor keeps the monitoring Ractor alive
ko1 Aug 4, 2026
7e1a720
[ruby/json] Widen the json_decode_integer fast path to more 64-bit in…
samyron Aug 6, 2026
fb7a246
Update to ruby/spec@4731fc6
andrykonchin Aug 6, 2026
9e985ff
Omit direct ISeq#to_binary tests when coverage is enabled
znz Aug 7, 2026
18efd4c
GC: only look at the GC mode when the allocation region runs out
ko1 Aug 7, 2026
b62b9b6
Add missing wrapper for `rb_deprecate_constant`
nobu Aug 7, 2026
2b75f70
Fix warnings
nobu Aug 7, 2026
a814061
[ruby/io-console] Introduce the `IO::Console` namespace
nobu Aug 7, 2026
6e65742
Add Proc#syntax_tree, Method#syntax_tree, and UnboundMethod#syntax_tree
mame Jul 18, 2026
6408f14
Add Thread::Backtrace::Location#syntax_tree
mame Jul 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ They are still available on rubygems.org and can be installed with
* 1.9.2 to [v1.9.3][win32ole-v1.9.3]
* irb 1.18.0
* 1.16.0 to [v1.17.0][irb-v1.17.0], [v1.18.0][irb-v1.18.0]
* reline 0.7.0

### RubyGems and Bundler

Expand Down Expand Up @@ -302,6 +303,25 @@ The following APIs, which have been deprecated for many years, are removed.

A lot of work has gone into making Ractors more stable, performant, and usable. These improvements bring Ractor implementation closer to leaving experimental status.

* The default GC now runs **per Ractor**: each Ractor collects its own heap
on its own thread without stopping the others, and a stop-the-world
collection only runs when it is really needed (explicit full `GC.start`,
shareable-object growth, reclaiming dead Ractors' heaps). Allocation-heavy
Ractor programs now scale like forked processes.

Visible behavior changes:

* `Ractor#value` returns the value only once; a second call raises
`Ractor::Error`.
* `GC.disable`/`GC.enable` act as per-Ractor holds on a process-wide
switch: one Ractor's `GC.enable` no longer overrides another Ractor's
`GC.disable`.
* `ObjectSpace.each_object` enumerates the calling Ractor's own objects
plus other Ractors' shareable objects (`ObjectSpace.dump_all` still
covers everything).
* `ObjectSpace.define_finalizer` on another Ractor's object raises
`Ractor::IsolationError`.

## JIT

[Feature #8948]: https://bugs.ruby-lang.org/issues/8948
Expand Down
46 changes: 46 additions & 0 deletions ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,42 @@ rb_ast_node_source_location(VALUE source, VALUE path, int first_lineno,
return true;
}

static VALUE
ast_node_find(rb_execution_context_t *ec, VALUE self, VALUE root, VALUE node_id)
{
return node_find(root, NUM2INT(node_id));
}

static VALUE
ast_node_source_hash(rb_execution_context_t *ec, VALUE self, VALUE node)
{
struct ASTNodeData *data;
TypedData_Get_Struct(node, struct ASTNodeData, &rb_node_type, data);

rb_ast_t *ast = rb_ruby_ast_data_get(data->ast_value);
if (!ast->body.has_source_hash) return Qnil;
return ULL2NUM(ast->body.source_hash);
}

extern VALUE rb_e_script;

static VALUE
iseq_compiled_by_prism_p(rb_execution_context_t *ec, VALUE self)
{
return RBOOL(ISEQ_BODY(rb_iseqw_to_iseq(self))->prism);
}

static VALUE
source_hash_of(rb_execution_context_t *ec, VALUE self, VALUE str)
{
StringValue(str);

rb_source_hash_state_t state;
rb_source_hash_init(&state);
rb_source_hash_update(&state, (const uint8_t *)RSTRING_PTR(str), (size_t)RSTRING_LEN(str));
return ULL2NUM(rb_source_hash_finalize(&state));
}

static VALUE
node_id_for_backtrace_location(rb_execution_context_t *ec, VALUE module, VALUE location)
{
Expand All @@ -278,6 +312,18 @@ node_id_for_backtrace_location(rb_execution_context_t *ec, VALUE module, VALUE l
return INT2NUM(node_id);
}

static VALUE
iseq_of_backtrace_location(rb_execution_context_t *ec, VALUE module, VALUE location)
{
if (!rb_frame_info_p(location)) {
rb_raise(rb_eTypeError, "Thread::Backtrace::Location object expected");
}

const rb_iseq_t *iseq = rb_get_iseq_from_frame_info(location);
if (!iseq) return Qnil;
return rb_iseqw_new(iseq);
}

static VALUE
ast_s_of(rb_execution_context_t *ec, VALUE module, VALUE body, VALUE keep_script_lines, VALUE error_tolerant, VALUE keep_tokens)
{
Expand Down
176 changes: 176 additions & 0 deletions ast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,179 @@ def inspect
end
end
end

class RubyVM::InstructionSequence
# call-seq:
# iseq.syntax_tree -> Prism::Node | RubyVM::AbstractSyntaxTree::Node | nil
#
# Returns the AST node that this instruction sequence was compiled from,
# by re-parsing the source with the same parser that compiled it: a
# Prism::Node if it was compiled by prism, or a
# RubyVM::AbstractSyntaxTree::Node if it was compiled by parse.y.
#
# Returns +nil+ whenever the node cannot be retrieved reliably. For
# example: the source is not available (such as eval'ed code without
# RubyVM.keep_script_lines enabled), or the source file has been modified
# since it was compiled.
#
# When a prism gem other than the default gem is loaded, a warning is
# emitted in verbose mode. In that case, the loaded prism may parse the
# source differently from the parser that compiled the instruction
# sequence, and the returned node may not correspond to the code that
# was actually executed.
#
# This method is experimental and might change without notice.
def syntax_tree
source_hash = self.source_hash
return nil unless source_hash

# When the source is kept in memory (RubyVM.keep_script_lines), use it
# instead of the file. This also works for eval'ed code.
if (lines = script_lines)
source = lines.join
else
path = absolute_path
return nil unless path && File.file?(path)
end

node_id = self.node_id
if Primitive.iseq_compiled_by_prism_p
require "prism"

# Only the default gem prism is the same parser as the one built into
# the interpreter. Another prism gem is still likely to parse the
# source in the same way, so continue with a warning.
if $VERBOSE && (spec = defined?(Gem) && Gem.loaded_specs["prism"]) && !spec.default_gem?
warn "syntax_tree: a prism gem other than the default gem is loaded; " \
"the result may not correspond exactly to the compiled code"
end

begin
result = source ? Prism.parse(source, version: "current") : Prism.parse_file(path, version: "current")
rescue ArgumentError
# The loaded prism does not know the grammar of the running Ruby.
return nil
end
return nil unless result.success?

# Hash exactly the bytes that prism parsed. The data section after an
# __END__ marker is not part of the code, so the hash covers the source
# only up to the end of the __END__ line.
code = result.source.source
if (data_loc = result.data_loc) && (eol = code.byteindex("\n", data_loc.start_offset))
code = code.byteslice(0, eol + 1)
end
return nil unless Primitive.source_hash_of(code) == source_hash

root = result.value
else
begin
root = source ? RubyVM::AbstractSyntaxTree.parse(source, keep_script_lines: true) :
RubyVM::AbstractSyntaxTree.parse_file(path, keep_script_lines: true)
rescue SyntaxError
# The source has been modified into invalid Ruby.
return nil
end
return nil unless Primitive.ast_node_source_hash(root) == source_hash

return Primitive.ast_node_find(root, node_id)
end

return root if root.node_id == node_id

queue = [root]
while (node = queue.shift)
node.compact_child_nodes.each do |child|
if child.node_id == node_id
# A block iseq refers to the block node itself. Return the outer
# node that owns the block (a CallNode, SuperNode, or
# ForwardingSuperNode) instead.
return child.type == :block_node ? node : child
end
queue << child
end
end

nil
end
end

class Proc
# call-seq:
# prc.syntax_tree -> Prism::Node | RubyVM::AbstractSyntaxTree::Node | nil
#
# Returns the AST node that this proc was compiled from. See
# RubyVM::InstructionSequence#syntax_tree for details and for when +nil+
# is returned.
#
# This method is experimental and might change without notice.
def syntax_tree
RubyVM::InstructionSequence.of(self)&.syntax_tree
end
end

class Method
# call-seq:
# meth.syntax_tree -> Prism::Node | RubyVM::AbstractSyntaxTree::Node | nil
#
# Returns the AST node that this method was compiled from. Returns
# +nil+ for methods not written in Ruby. See
# RubyVM::InstructionSequence#syntax_tree for other cases where +nil+ is
# returned.
#
# This method is experimental and might change without notice.
def syntax_tree
RubyVM::InstructionSequence.of(self)&.syntax_tree
end
end

class UnboundMethod
# call-seq:
# meth.syntax_tree -> Prism::Node | RubyVM::AbstractSyntaxTree::Node | nil
#
# Returns the AST node that this method was compiled from. Returns
# +nil+ for methods not written in Ruby. See
# RubyVM::InstructionSequence#syntax_tree for other cases where +nil+ is
# returned.
#
# This method is experimental and might change without notice.
def syntax_tree
RubyVM::InstructionSequence.of(self)&.syntax_tree
end
end

class Thread::Backtrace::Location
# call-seq:
# location.syntax_tree -> Prism::Node | RubyVM::AbstractSyntaxTree::Node | nil
#
# Returns the AST node at this location, by re-parsing the source file. See
# RubyVM::InstructionSequence#syntax_tree for when +nil+ is returned.
#
# This method is experimental and might change without notice.
def syntax_tree
iseq = Primitive.iseq_of_backtrace_location(self)
return nil unless iseq

node_id = Primitive.node_id_for_backtrace_location(self)
return nil unless node_id

scope = iseq.syntax_tree
return nil unless scope

if scope.is_a?(RubyVM::AbstractSyntaxTree::Node)
return Primitive.ast_node_find(scope, node_id)
end

return scope if scope.node_id == node_id

queue = [scope]
while (node = queue.shift)
node.compact_child_nodes.each do |child|
return child if child.node_id == node_id
queue << child
end
end

nil
end
end
Loading