Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
411cfc4
Do not resolve dependency names against the build directory (#18212)
k0kubun Aug 6, 2026
1db6f0f
[ruby/prism] Split up newline token
kddnewton Aug 6, 2026
6b93405
[ruby/strscan] Return nil for empty integer captures
nobu Aug 6, 2026
6251e06
[ruby/strscan] Handle shrunk captures on TruffleRuby
nobu Aug 6, 2026
918fed9
[ruby/prism] Revert "Split up newline token"
kddnewton Aug 6, 2026
40e2c0f
[ruby/strscan] Fix `charpos` when the stored string is shrunk
nobu Aug 5, 2026
a57cc37
[ruby/time] Require colon in timezone offset for Time.rfc3339
jeremyevans Aug 6, 2026
10fa3a5
Bump the github-actions group across 1 directory with 3 updates
dependabot[bot] Aug 6, 2026
82294a7
[Bug #22223] Check SO_ERROR after waiting for nonblocking connect
hsbt Aug 5, 2026
ef6b630
Finish fix for cross-compilation build race condition
pbrkr Jul 31, 2026
c3c0e28
Do not consume BUNDLER_SETUP outside the main box
hsbt Aug 6, 2026
8268411
Note when the BUNDLER_SETUP require can be dropped
hsbt Aug 6, 2026
eec8cf0
Test BUNDLER_SETUP loading in boxes
hsbt Aug 6, 2026
4c5ba84
Make the autoload test portable to Ruby 4.0
hsbt Aug 6, 2026
1d9b129
[DOC] Deprecate ruby2_keywords
shugo Jul 24, 2026
a3c7ff6
win32: single-syscall fast path for winnt_stat (Win10-safe)
Largo Jun 16, 2026
a7512f0
[Bug #19378] win32: Rework stat fast path
hsbt Aug 4, 2026
39e43e1
[Feature #22213] Allow no-argument and chained calls of Proc#refined
shugo Jul 24, 2026
1ba809e
C API: Expose `rb_iseq_load_from_binary`
byroot Jul 31, 2026
5b505f8
Reduce RHASH_TBL_RAW usage
byroot Aug 6, 2026
66c00fd
rb_hash_init: don't spill to `st_table` unless needed
byroot Aug 5, 2026
84a9e6b
[Feature #22212] Add Thread::Backtrace::Location#source_range
eregon Jul 23, 2026
93638d0
Compute a source hash of the program in parse.y
mame Jul 18, 2026
2d62952
Compute a source hash for prism as well
mame Jul 18, 2026
25959fe
Store a source hash in ISeqs
mame Jul 18, 2026
a55e31b
Remove unused ISeq methods
eregon Jul 29, 2026
eb0254e
Check the source hash for Thread::Backtrace::Location#source_range
eregon Jul 29, 2026
a89b3a6
[Bug #22197] Show the original definition module in backtrace labels
eregon Jul 24, 2026
c0ed960
[Bug #22197] Record the definition module on the method definition
eregon Jul 16, 2026
2461cae
[Feature #22118] Introduce Basic Bit Operations into String (#17353)
hasumikin Aug 6, 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
6 changes: 3 additions & 3 deletions .github/workflows/check_sast.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ jobs:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
uses: github/codeql-action/init@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5
with:
languages: ${{ matrix.language }}
build-mode: none
config-file: .github/codeql/codeql-config.yml

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
uses: github/codeql-action/analyze@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5
with:
category: '/language:${{ matrix.language }}'
upload: False
Expand Down Expand Up @@ -127,7 +127,7 @@ jobs:
continue-on-error: true

- name: Upload SARIF
uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
uses: github/codeql-action/upload-sarif@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5
with:
sarif_file: sarif-results/${{ matrix.language }}.sarif
continue-on-error: true
2 changes: 1 addition & 1 deletion .github/workflows/scorecards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
uses: github/codeql-action/upload-sarif@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5
with:
sarif_file: results.sarif
18 changes: 18 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ Note: We're only listing outstanding class updates.
given names, raising `KeyError` for missing names unless a block is
given. [[Feature #21781]]

* Hash

* `Hash.ruby2_keywords_hash?` and `Hash.ruby2_keywords_hash` are
deprecated and will be removed in Ruby 4.5. [[Feature #22205]]

* Integer

* `Integer#bit_count` is added. It returns the number of `1` bits in the
Expand All @@ -59,6 +64,11 @@ Note: We're only listing outstanding class updates.
* `MatchData#integer_at` is added. It converts the matched substring to
integer and return the result. [[Feature #21932]]

* Module

* `Module#ruby2_keywords` and top-level `ruby2_keywords` are
deprecated and will be removed in Ruby 4.4. [[Feature #22205]]

* ObjectSpace

* `ObjectSpace._id2ref` was removed. [[Feature #22135]]
Expand All @@ -69,6 +79,8 @@ Note: We're only listing outstanding class updates.
receiver but with the refinements activated by the given modules
in effect inside its body, without affecting the original `Proc`.
[[Feature #22097]]
* `Proc#ruby2_keywords` is deprecated and will be removed in Ruby 4.4.
[[Feature #22205]]

* Range

Expand Down Expand Up @@ -101,6 +113,11 @@ Note: We're only listing outstanding class updates.

* `Symbol#to_s` now returns a frozen string. [[Feature #22137]]

* Thread::Backtrace::Location

* `Thread::Backtrace::Location#source_range` is added. It returns a
`Ruby::SourceRange` for the Ruby expression associated with the frame.

## Stdlib updates

* Psych
Expand Down Expand Up @@ -300,6 +317,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable.
[Feature #22139]: https://bugs.ruby-lang.org/issues/22139
[Feature #22175]: https://bugs.ruby-lang.org/issues/22175
[Feature #22185]: https://bugs.ruby-lang.org/issues/22185
[Feature #22205]: https://bugs.ruby-lang.org/issues/22205
[PR #17201]: https://github.com/ruby/ruby/pull/17201
[GH-psych #805]: https://github.com/ruby/psych/pull/805
[RubyGems-v4.0.4]: https://github.com/rubygems/rubygems/releases/tag/v4.0.4
Expand Down
68 changes: 62 additions & 6 deletions ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -179,28 +179,84 @@ rb_ast_parse_array(VALUE array, VALUE keep_script_lines, VALUE error_tolerant, V

static VALUE node_children(VALUE, const NODE*);

static VALUE
node_find(VALUE self, const int node_id)
struct node_find_result {
VALUE node;
VALUE parent;
};

static bool
node_find_with_parent(VALUE self, VALUE parent, const int node_id, struct node_find_result *result)
{
VALUE ary;
long i;
struct ASTNodeData *data;
TypedData_Get_Struct(self, struct ASTNodeData, &rb_node_type, data);

if (nd_node_id(data->node) == node_id) return self;
if (nd_node_id(data->node) == node_id) {
result->node = self;
result->parent = parent;
return true;
}

ary = node_children(data->ast_value, data->node);

for (i = 0; i < RARRAY_LEN(ary); i++) {
VALUE child = RARRAY_AREF(ary, i);

if (CLASS_OF(child) == rb_cNode) {
VALUE result = node_find(child, node_id);
if (RTEST(result)) return result;
if (node_find_with_parent(child, self, node_id, result)) return true;
}
}

return false;
}

static VALUE
node_find(VALUE self, const int node_id)
{
struct node_find_result result = { Qnil, Qnil };
node_find_with_parent(self, Qnil, node_id, &result);
return result.node;
}

bool
rb_ast_node_source_location(VALUE source, VALUE path, int first_lineno,
int node_id, bool block_iseq, int iseq_node_id,
rb_code_location_t *location)
{
StringValue(source);
VALUE vparser = setup_vparser(Qfalse, Qfalse, Qfalse);
VALUE ast_value = rb_parser_compile_string_path(vparser, path, source, first_lineno);
VALUE ast = ast_parse_done(ast_value);

struct node_find_result result = { Qnil, Qnil };
if (!node_find_with_parent(ast, Qnil, node_id, &result)) return false;

struct ASTNodeData *data;
TypedData_Get_Struct(result.node, struct ASTNodeData, &rb_node_type, data);
const NODE *node = data->node;

if (!NIL_P(result.parent)) {
struct ASTNodeData *parent_data;
TypedData_Get_Struct(result.parent, struct ASTNodeData, &rb_node_type, parent_data);
const NODE *parent = parent_data->node;

/* Prism's call node includes its literal block. */
if (nd_type(parent) == NODE_ITER && RNODE_ITER(parent)->nd_iter == node) {
node = parent;
}
}

/* Prism's block node excludes the call that produced the block. */
if (block_iseq && node_id == iseq_node_id && nd_type(node) == NODE_ITER) {
const NODE *scope = RNODE_ITER(node)->nd_body;
if (scope && nd_type(scope) == NODE_SCOPE) {
node = scope;
}
}

return Qnil;
*location = *nd_code_loc(node);
return true;
}

extern VALUE rb_e_script;
Expand Down
30 changes: 29 additions & 1 deletion compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -1503,6 +1503,14 @@ new_child_iseq(rb_iseq_t *iseq, const NODE *const node,
rb_iseq_t *ret_iseq;
VALUE ast_value = rb_ruby_ast_new(node);

// The child AST wrapper does not carry the source hash, so copy it from
// the enclosing iseq before compiling, for grandchildren to inherit it.
if (ISEQ_BODY(iseq)->has_source_hash) {
rb_ast_t *child_ast = rb_ruby_ast_data_get(ast_value);
child_ast->body.source_hash = ISEQ_BODY(iseq)->source_hash;
child_ast->body.has_source_hash = 1;
}

debugs("[new_child_iseq]> ---------------------------------------\n");
int isolated_depth = ISEQ_COMPILE_DATA(iseq)->isolated_depth;
ret_iseq = rb_iseq_new_with_opt(ast_value, name,
Expand Down Expand Up @@ -12476,6 +12484,12 @@ rb_iseq_build_from_ary(rb_iseq_t *iseq, VALUE misc, VALUE locals, VALUE params,
#undef INT_PARAM
}

VALUE source_hash = rb_hash_aref(misc, ID2SYM(rb_intern("source_hash")));
if (!NIL_P(source_hash)) {
ISEQ_BODY(iseq)->source_hash = NUM2ULL(source_hash);
ISEQ_BODY(iseq)->has_source_hash = true;
}

VALUE node_ids = Qfalse;
#ifdef USE_ISEQ_NODE_ID
node_ids = rb_hash_aref(misc, ID2SYM(rb_intern("node_ids")));
Expand Down Expand Up @@ -12602,7 +12616,7 @@ typedef uint32_t ibf_offset_t;

#define IBF_MAJOR_VERSION ISEQ_MAJOR_VERSION
#ifdef RUBY_DEVEL
#define IBF_DEVEL_VERSION 5
#define IBF_DEVEL_VERSION 6
#define IBF_MINOR_VERSION (ISEQ_MINOR_VERSION * 10000 + IBF_DEVEL_VERSION)
#else
#define IBF_MINOR_VERSION ISEQ_MINOR_VERSION
Expand Down Expand Up @@ -13782,6 +13796,12 @@ ibf_dump_iseq_each(struct ibf_dump *dump, const rb_iseq_t *iseq)
ibf_dump_write_small_value(dump, location_label_index);
ibf_dump_write_small_value(dump, body->location.first_lineno);
ibf_dump_write_small_value(dump, body->location.node_id);
/* Dump the source hash in two 32-bit halves, because VALUE may be
* 32 bits wide. */
uint64_t source_hash = body->has_source_hash ? body->source_hash : 0;
ibf_dump_write_small_value(dump, (VALUE)(uint32_t)(source_hash >> 32));
ibf_dump_write_small_value(dump, (VALUE)(uint32_t)source_hash);
ibf_dump_write_small_value(dump, body->has_source_hash ? 1 : 0);
ibf_dump_write_small_value(dump, body->location.code_location.beg_pos.lineno);
ibf_dump_write_small_value(dump, body->location.code_location.beg_pos.column);
ibf_dump_write_small_value(dump, body->location.code_location.end_pos.lineno);
Expand Down Expand Up @@ -13894,6 +13914,10 @@ ibf_load_iseq_each(struct ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset)
const VALUE location_label_index = ibf_load_small_value(load, &reading_pos);
const int location_first_lineno = (int)ibf_load_small_value(load, &reading_pos);
const int location_node_id = (int)ibf_load_small_value(load, &reading_pos);
const uint64_t source_hash_hi = (uint64_t)ibf_load_small_value(load, &reading_pos);
const uint64_t source_hash_lo = (uint64_t)ibf_load_small_value(load, &reading_pos);
const uint64_t source_hash = (source_hash_hi << 32) | (uint32_t)source_hash_lo;
const bool has_source_hash = ibf_load_small_value(load, &reading_pos) != 0;
const int location_code_location_beg_pos_lineno = (int)ibf_load_small_value(load, &reading_pos);
const int location_code_location_beg_pos_column = (int)ibf_load_small_value(load, &reading_pos);
const int location_code_location_end_pos_lineno = (int)ibf_load_small_value(load, &reading_pos);
Expand Down Expand Up @@ -13994,6 +14018,8 @@ ibf_load_iseq_each(struct ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset)

load_body->location.first_lineno = location_first_lineno;
load_body->location.node_id = location_node_id;
load_body->source_hash = source_hash;
load_body->has_source_hash = has_source_hash;
load_body->location.code_location.beg_pos.lineno = location_code_location_beg_pos_lineno;
load_body->location.code_location.beg_pos.column = location_code_location_beg_pos_column;
load_body->location.code_location.end_pos.lineno = location_code_location_end_pos_lineno;
Expand Down Expand Up @@ -15209,6 +15235,8 @@ rb_iseq_dup_with_independent_caches(const rb_iseq_t *src_root)
rb_ibf_load_iseq_complete(copy);
}

FL_SET((VALUE)copy, ISEQ_REFINED_COPY);

struct rb_iseq_constant_body *cb = ISEQ_BODY(copy);
if (!cb->local_iseq) RB_OBJ_WRITE(copy, &cb->local_iseq, sb->local_iseq);
RB_OBJ_WRITE(copy, &cb->location.pathobj, sb->location.pathobj);
Expand Down
6 changes: 2 additions & 4 deletions cont.c
Original file line number Diff line number Diff line change
Expand Up @@ -2637,7 +2637,6 @@ rb_fiber_start(rb_fiber_t *fiber_arg)
rb_fiber_t * volatile fiber = fiber_arg;
rb_thread_t * volatile th = fiber->cont.saved_ec.thread_ptr;

rb_proc_t *proc;
enum ruby_tag_type state;

VM_ASSERT(th->ec == GET_EC());
Expand All @@ -2647,12 +2646,10 @@ rb_fiber_start(rb_fiber_t *fiber_arg)
th->blocking += 1;
}

/* resolved before EC_PUSH_TAG to keep the setjmp region minimal */
const rb_cref_t *cref = rb_proc_refinements_cref(fiber->first_proc);

EC_PUSH_TAG(th->ec);
if ((state = EC_EXEC_TAG()) == TAG_NONE) {
rb_context_t *cont = &fiber->cont;
rb_proc_t *proc;
int argc;
const VALUE *argv, args = cont->value;
GetProcPtr(fiber->first_proc, proc);
Expand All @@ -2663,6 +2660,7 @@ rb_fiber_start(rb_fiber_t *fiber_arg)
th->ec->root_svar = Qfalse;

EXEC_EVENT_HOOK(th->ec, RUBY_EVENT_FIBER_SWITCH, th->self, 0, 0, 0, Qnil);
const rb_cref_t *cref = rb_proc_refinements_cref_for_call(fiber->first_proc);
cont->value = rb_vm_invoke_proc(th->ec, proc, argc, argv, cont->kw_splat, VM_BLOCK_HANDLER_NONE, cref);
}
EC_POP_TAG();
Expand Down
2 changes: 2 additions & 0 deletions defs/gmake.mk
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ endif

ifeq ($(HAVE_GIT),yes)
REVISION_LATEST := $(shell $(GIT_IN_SRC) rev-parse HEAD 2>/dev/null)
ifneq ($(REVISION_LATEST),)
REVISION_IN_HEADER := $(shell sed '/^\#define RUBY_FULL_REVISION "\(.*\)"/!d;s//\1/;q' $(wildcard $(srcdir)/revision.h revision.h) /dev/null 2>/dev/null)
ifeq ($(REVISION_IN_HEADER),)
REVISION_IN_HEADER := none
Expand All @@ -444,6 +445,7 @@ ifneq ($(REVISION_IN_HEADER),$(REVISION_LATEST))
$(REVISION_H): PHONY
endif
endif
endif

include $(top_srcdir)/yjit/yjit.mk
include $(top_srcdir)/zjit/zjit.mk
Expand Down
16 changes: 16 additions & 0 deletions doc/string/bit_clear.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Sets the bit at zero-based bit +offset+ to 0; returns +self+:

s = "\xFF"
s.bit_clear(1) # => "\xFD"
s # => "\xFD"

By default, bits within each byte are numbered from least-significant to
most-significant. If +lsb_first+ is +false+, byte order is unchanged but bits
within each byte are numbered from most-significant to least-significant:

s = "\xFF"
s.bit_clear(1, lsb_first: false) # => "\xBF"

Raises +IndexError+ if +offset+ is out of range.
Raises +ArgumentError+ if +offset+ is too large to be represented.
Raises +ArgumentError+ if +lsb_first+ is neither +true+ nor +false+.
8 changes: 8 additions & 0 deletions doc/string/bit_count.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Returns the number of set bits in +self+:

"\x00".bit_count # => 0
"\xFF".bit_count # => 8
"\xAA".bit_count # => 4

The count is over the bytes of +self+ and is independent of string encoding.
Raises +ArgumentError+ if any argument is given.
16 changes: 16 additions & 0 deletions doc/string/bit_flip.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Flips the bit at zero-based bit +offset+; returns +self+:

s = "\x00"
s.bit_flip(1) # => "\x02"
s.bit_flip(1) # => "\x00"

By default, bits within each byte are numbered from least-significant to
most-significant. If +lsb_first+ is +false+, byte order is unchanged but bits
within each byte are numbered from most-significant to least-significant:

s = "\x00"
s.bit_flip(1, lsb_first: false) # => "\x40"

Raises +IndexError+ if +offset+ is out of range.
Raises +ArgumentError+ if +offset+ is too large to be represented.
Raises +ArgumentError+ if +lsb_first+ is neither +true+ nor +false+.
20 changes: 20 additions & 0 deletions doc/string/bit_get.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Returns +0+ or +1+ for the bit at zero-based bit +offset+:

s = "\xAA" # 0b10101010
s.bit_get(0) # => 0
s.bit_get(1) # => 1

Returns +nil+ if +offset+ is beyond the end of +self+:

s.bit_get(8) # => nil

By default, bits within each byte are numbered from least-significant to
most-significant. If +lsb_first+ is +false+, byte order is unchanged but bits
within each byte are numbered from most-significant to least-significant:

s.bit_get(0, lsb_first: false) # => 1
s.bit_get(1, lsb_first: false) # => 0

Raises +IndexError+ if +offset+ is negative.
Raises +ArgumentError+ if +offset+ is too large to be represented.
Raises +ArgumentError+ if +lsb_first+ is neither +true+ nor +false+.
16 changes: 16 additions & 0 deletions doc/string/bit_set.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Sets the bit at zero-based bit +offset+ to 1; returns +self+:

s = "\x00"
s.bit_set(1) # => "\x02"
s # => "\x02"

By default, bits within each byte are numbered from least-significant to
most-significant. If +lsb_first+ is +false+, byte order is unchanged but bits
within each byte are numbered from most-significant to least-significant:

s = "\x00"
s.bit_set(1, lsb_first: false) # => "\x40"

Raises +IndexError+ if +offset+ is out of range.
Raises +ArgumentError+ if +offset+ is too large to be represented.
Raises +ArgumentError+ if +lsb_first+ is neither +true+ nor +false+.
Loading