diff --git a/NEWS.md b/NEWS.md
index 5fc669c630797e..5d3ec3a703d9c7 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -39,6 +39,12 @@ Note: We're only listing outstanding class updates.
given names, raising `KeyError` for missing names unless a block is
given. [[Feature #21781]]
+* Integer
+
+ * `Integer#bit_count` is added. It returns the number of `1` bits in the
+ binary representation of a non-negative integer (its population count).
+ [[Feature #20163]]
+
* Kernel
* `Kernel#autoload_relative` and `Module#autoload_relative` are added.
@@ -242,6 +248,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable.
[Feature #8948]: https://bugs.ruby-lang.org/issues/8948
[Feature #15330]: https://bugs.ruby-lang.org/issues/15330
+[Feature #20163]: https://bugs.ruby-lang.org/issues/20163
[Feature #21390]: https://bugs.ruby-lang.org/issues/21390
[Feature #21768]: https://bugs.ruby-lang.org/issues/21768
[Feature #21781]: https://bugs.ruby-lang.org/issues/21781
diff --git a/dir.c b/dir.c
index c24030b59f9f0e..d9536b200ba22b 100644
--- a/dir.c
+++ b/dir.c
@@ -3731,14 +3731,26 @@ dir_collect_children(VALUE dir)
/*
* call-seq:
- * children -> array
+ * scan -> entries
+ * scan {|entry_name, entry_type| ... } -> nil
*
- * Returns an array of the entry names in +self+ along with their type
- * except for '.' and '..':
+ * Scans the entries in +self+, except for '.' and '..'.
+ *
+ * With no block given, returns +entries+, an array of 2-element arrays.
+ * Each nested array contains an entry name and its type:
*
* dir = Dir.new('/example')
* dir.scan # => [["config.h", :file], ["lib", :directory], ["main.rb", :file]]
*
+ * With a block given, calls the block with each entry name and its type;
+ * returns +nil+:
+ *
+ * entries = []
+ * dir.scan do |entry_name, entry_type|
+ * entries << [entry_name, entry_type]
+ * end # => nil
+ * entries # => [["config.h", :file], ["lib", :directory], ["main.rb", :file]]
+ *
*/
static VALUE
dir_scan_children(VALUE dir)
@@ -3784,25 +3796,36 @@ dir_s_children(int argc, VALUE *argv, VALUE io)
/*
* call-seq:
+ * Dir.scan(dirpath) -> entries
+ * Dir.scan(dirpath, encoding: 'UTF-8') -> entries
* Dir.scan(dirpath) {|entry_name, entry_type| ... } -> nil
* Dir.scan(dirpath, encoding: 'UTF-8') {|entry_name, entry_type| ... } -> nil
- * Dir.scan(dirpath) -> [[entry_name, entry_type], ...]
- * Dir.scan(dirpath, encoding: 'UTF-8') -> [[entry_name, entry_type], ...]
*
- * Yields or returns an array of the entry names in the directory at +dirpath+
- * associated with their type, except for '.' and '..';
- * sets the given encoding onto each returned entry name.
+ * Scans the entries in the directory at +dirpath+, except for '.'
+ * and '..'.
+ *
+ * With no block given, returns +entries+, an array of 2-element arrays.
+ * Each nested array contains an entry name and its type:
*
- * The type symbol is one of:
- * ``:file'', ``:directory'',
- * ``:characterSpecial'', ``:blockSpecial'',
- * ``:fifo'', ``:link'',
- * or ``:socket'':
+ * Dir.scan('/example') # => [["config.h", :file], ["lib", :directory], ["main.rb", :file]]
*
- * Dir.children('/example') # => [["config.h", :file], ["lib", :directory], ["main.rb", :file]]
- * Dir.children('/example').first.first.encoding
+ * With a block given, calls the block with each entry name and its type;
+ * returns +nil+:
+ *
+ * entries = []
+ * Dir.scan('/example') do |entry_name, entry_type|
+ * entries << [entry_name, entry_type]
+ * end # => nil
+ * entries # => [["config.h", :file], ["lib", :directory], ["main.rb", :file]]
+ *
+ * The type symbol is one of +:file+, +:directory+, +:characterSpecial+,
+ * +:blockSpecial+, +:fifo+, +:link+, +:socket+, or +:unknown+.
+ *
+ * The given +encoding+ is used as the external encoding for each entry name:
+ *
+ * Dir.scan('/example').first.first.encoding
* # => #
- * Dir.children('/example', encoding: 'US-ASCII').first.encoding
+ * Dir.scan('/example', encoding: 'US-ASCII').first.first.encoding
* # => #
*
* See {String Encoding}[rdoc-ref:encodings.rdoc@String+Encoding].
diff --git a/doc/contributing/documentation_guide.md b/doc/contributing/documentation_guide.md
index 7c73ad1c50b0f2..e2d1f590247a10 100644
--- a/doc/contributing/documentation_guide.md
+++ b/doc/contributing/documentation_guide.md
@@ -27,7 +27,7 @@ the browser when you edit source files:
make html-server
```
-Then visit http://localhost:4000 in your browser.
+Then visit `http://localhost:4000` in your browser.
To use a different port: `make html-server RDOC_SERVER_PORT=8080`.
If you don't have a build directory, follow the [quick start
diff --git a/doc/maintainers.md b/doc/maintainers.md
index 51aeb41d13db30..494716c6dba5e0 100644
--- a/doc/maintainers.md
+++ b/doc/maintainers.md
@@ -126,7 +126,7 @@ consensus on ruby-core/ruby-dev.
* *No maintainer*
* https://github.com/ruby/English
-* https://rubygems.org/gems/English
+* https://rubygems.org/gems/english
#### lib/delegate.rb
diff --git a/pathname_builtin.rb b/pathname_builtin.rb
index c6edf14e58039c..d20c5b0b3228dd 100644
--- a/pathname_builtin.rb
+++ b/pathname_builtin.rb
@@ -2283,7 +2283,6 @@ def world_readable?() File.world_readable?(@path) end
# :markup: markdown
#
# call-seq:
- #
# readable_real? -> true or false
#
# Like #readable?, but checks against the real user and group ids
@@ -2425,7 +2424,24 @@ def sticky?() FileTest.sticky?(@path) end
#
def symlink?() FileTest.symlink?(@path) end
- # See FileTest.writable?.
+ # :markup: markdown
+ #
+ # call-seq:
+ # writable? => true or false
+ #
+ # Returns whether the path in `self` points to an entry that is writable
+ # by the owner and group of the current process:
+ #
+ # ```ruby
+ # pn = Pathname('/tmp/secret.txt')
+ # pn.write('foo')
+ # pn.writable? # => true
+ # pn.chmod(0o000)
+ # pn.writable? # => false
+ # pn.delete
+ # Pathname('nosuch').writable? # => false
+ # ```
+ #
def writable?() FileTest.writable?(@path) end
# :markup: markdown
@@ -2452,10 +2468,40 @@ def writable?() FileTest.writable?(@path) end
#
def world_writable?() File.world_writable?(@path) end
- # See FileTest.writable_real?.
+ # :markup: markdown
+ #
+ # call-seq:
+ # writable_real? -> true or false
+ #
+ # Like #writable?, but checks against the real user and group ids
+ # instead of the effective ids.
def writable_real?() FileTest.writable_real?(@path) end
- # See FileTest.zero?.
+ # :markup: markdown
+ #
+ # call-seq:
+ # zero? -> true or false
+ #
+ # Returns whether the entry represented by `self` exists and has size zero:
+ #
+ # ```
+ # dir_pn = Pathname('example_dir')
+ # dir_pn.zero? # => false # Dir does not exist.
+ # dir_pn.mkdir
+ # dir_pn.zero? # => false # Directory never has size zero.
+ # dir_pn.empty? # => true # But this one is empty.
+ #
+ # file_pn = Pathname('example_dir/example.txt')
+ # file_pn.zero? # => false # File does not exist.
+ # file_pn.write('')
+ # file_pn.zero? # => true
+ # file_pn.write('foo')
+ # file_pn.zero? # => false
+ #
+ # file_pn.delete
+ # dir_pn.delete
+ # ```
+ #
def zero?() FileTest.zero?(@path) end
end
diff --git a/set.c b/set.c
index 379abadee8bec1..289898a6fc1041 100644
--- a/set.c
+++ b/set.c
@@ -1472,6 +1472,7 @@ set_i_xor(VALUE set, VALUE other)
set_merge_enum_into(tmp, other);
set_iter(tmp, set_xor_i, (st_data_t)new_set);
}
+ set_compact_after_delete(set);
return new_set;
}
@@ -1523,6 +1524,7 @@ set_remove_enum_from(VALUE set, VALUE arg)
else {
rb_block_call(arg, enum_method_id(arg), 0, 0, set_remove_block, (VALUE)set);
}
+ set_compact_after_delete(set);
}
/*
@@ -1667,6 +1669,7 @@ set_i_keep_if(VALUE set)
rb_check_frozen(set);
set_iter(set, set_keep_if_i, (st_data_t)RSET_TABLE(set));
+ set_compact_after_delete(set);
return set;
}
@@ -1696,6 +1699,7 @@ set_i_select(VALUE set)
set_table *table = RSET_TABLE(set);
size_t n = set_table_size(table);
set_iter(set, set_keep_if_i, (st_data_t)table);
+ set_compact_after_delete(set);
return (n == set_table_size(table)) ? Qnil : set;
}
@@ -1731,6 +1735,7 @@ set_i_replace(VALUE set, VALUE other)
set_table_clear(RSET_TABLE(set));
set_merge_enum_into(set, other);
}
+ set_compact_after_delete(set);
return set;
}
@@ -2384,7 +2389,6 @@ rb_set_size(VALUE set)
* - {Assigning}[rdoc-ref:Set@Methods+for+Assigning]
* - {Deleting}[rdoc-ref:Set@Methods+for+Deleting]
* - {Converting}[rdoc-ref:Set@Methods+for+Converting]
- * - {Iterating}[rdoc-ref:Set@Methods+for+Iterating]
* - {And more....}[rdoc-ref:Set@Other+Methods]
*
* === Methods for Creating a \Set
diff --git a/string.c b/string.c
index b54051f1a6f000..33377dc39497d3 100644
--- a/string.c
+++ b/string.c
@@ -5136,10 +5136,8 @@ static VALUE get_pat(VALUE);
*
* regexp = Regexp.new(pattern)
*
- * - Computes +matchdata+, which will be either a MatchData object or +nil+
- * (see Regexp#match):
- *
- * matchdata = regexp.match(self[offset..])
+ * - Calls regexp.match with +self+ to compute +matchdata+.
+ * If +offset+ is given, it is also passed (see Regexp#match).
*
* With no block given, returns the computed +matchdata+ or +nil+:
*
@@ -5187,8 +5185,8 @@ rb_str_match_m(int argc, VALUE *argv, VALUE str)
*
* regexp = Regexp.new(pattern)
*
- * Returns +true+ if self[offset..].match(regexp) returns a MatchData object,
- * +false+ otherwise:
+ * The search for +regexp+ in +self+ begins at the given character +offset+.
+ * Returns +true+ if a match is found, +false+ otherwise:
*
* 'foo'.match?(/o/) # => true
* 'foo'.match?('o') # => true
diff --git a/yjit/src/backend/arm64/mod.rs b/yjit/src/backend/arm64/mod.rs
index 1712ad0302c641..c6ded9abd29403 100644
--- a/yjit/src/backend/arm64/mod.rs
+++ b/yjit/src/backend/arm64/mod.rs
@@ -37,9 +37,16 @@ pub const C_SP_STEP: i32 = 16;
impl CodeBlock {
// The maximum number of bytes that can be generated by emit_jmp_ptr.
pub fn jmp_ptr_bytes(&self) -> usize {
+ Self::jmp_ptr_bytes_for_region(self.virtual_region_size())
+ }
+
+ // Split out of jmp_ptr_bytes() so the range boundary can be tested without
+ // allocating a region that large.
+ fn jmp_ptr_bytes_for_region(region_size: usize) -> usize {
// b instruction's offset is encoded as imm26 times 4. It can jump to
// +/-128MiB, so this can be used when --yjit-exec-mem-size <= 128.
- let num_insns = if b_offset_fits_bits(self.virtual_region_size() as i64 / 4) {
+ // The widest in-region branch spans the region minus one instruction.
+ let num_insns = if b_offset_fits_bits((region_size as i64 - 4) / 4) {
1 // b instruction
} else {
5 // 4 instructions to load a 64-bit absolute address + br instruction
@@ -1382,6 +1389,18 @@ mod tests {
(Assembler::new(0), CodeBlock::new_dummy(1024))
}
+ #[test]
+ fn test_jmp_ptr_bytes_at_b_range_boundary() {
+ // The widest branch in a 128MiB region spans 128MiB - 4 bytes, which is
+ // still the largest offset a b can encode, so one instruction is enough.
+ // This is the default region size, so it is the common case.
+ assert_eq!(4, CodeBlock::jmp_ptr_bytes_for_region(128 * 1024 * 1024));
+
+ // One instruction further and a b can no longer span the region, so we
+ // have to reserve room for an absolute load-address plus br.
+ assert_eq!(20, CodeBlock::jmp_ptr_bytes_for_region(128 * 1024 * 1024 + 4));
+ }
+
#[test]
fn test_emit_add() {
let (mut asm, mut cb) = setup_asm();
diff --git a/zjit/src/backend/arm64/mod.rs b/zjit/src/backend/arm64/mod.rs
index cc3eec92df10e0..f74098f900c1b9 100644
--- a/zjit/src/backend/arm64/mod.rs
+++ b/zjit/src/backend/arm64/mod.rs
@@ -99,21 +99,7 @@ impl From<&Opnd> for A64Opnd {
}
}
-/// Call emit_jmp_ptr and immediately invalidate the written range.
-/// This is needed when next_page also moves other_cb that is not invalidated
-/// by compile_with_regs. Doing it here allows you to avoid invalidating a lot
-/// more than necessary when other_cb jumps from a position early in the page.
-/// This invalidates a small range of cb twice, but we accept the small cost.
-fn emit_jmp_ptr_with_invalidation(cb: &mut CodeBlock, dst_ptr: CodePtr) {
- let start = cb.get_write_ptr();
- emit_jmp_ptr(cb, dst_ptr, true);
- let end = cb.get_write_ptr();
- trace_compile_phase("invalidate_icache", || {
- unsafe { rb_jit_icache_invalidate(start.raw_ptr(cb) as _, end.raw_ptr(cb) as _) };
- });
-}
-
-fn emit_jmp_ptr(cb: &mut CodeBlock, dst_ptr: CodePtr, padding: bool) {
+fn emit_jmp_ptr(cb: &mut CodeBlock, dst_ptr: CodePtr) {
let src_addr = cb.get_write_ptr().as_offset();
let dst_addr = dst_ptr.as_offset();
@@ -121,7 +107,7 @@ fn emit_jmp_ptr(cb: &mut CodeBlock, dst_ptr: CodePtr, padding: bool) {
// branch instruction. Otherwise, we'll move the
// destination into a register and use the branch
// register instruction.
- let num_insns = if b_offset_fits_bits((dst_addr - src_addr) / 4) {
+ if b_offset_fits_bits((dst_addr - src_addr) / 4) {
b(cb, InstructionOffset::from_bytes((dst_addr - src_addr) as i32));
1
} else {
@@ -129,16 +115,6 @@ fn emit_jmp_ptr(cb: &mut CodeBlock, dst_ptr: CodePtr, padding: bool) {
br(cb, Assembler::EMIT_OPND);
num_insns + 1
};
-
- if padding {
- // Make sure it's always a consistent number of
- // instructions in case it gets patched and has to
- // use the other branch.
- assert!(num_insns * 4 <= cb.jmp_ptr_bytes());
- for _ in num_insns..(cb.jmp_ptr_bytes() / 4) {
- nop(cb);
- }
- }
}
/// Emit the required instructions to load the given value into the
@@ -1478,7 +1454,7 @@ impl Assembler {
Insn::Jmp(target) => {
match *target {
Target::CodePtr(dst_ptr) => {
- emit_jmp_ptr(cb, dst_ptr, true);
+ emit_jmp_ptr(cb, dst_ptr);
},
Target::Label(label_idx) => {
// Reserve space for a single B instruction
diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs
index 3cd3a9cfa67819..22fde4a9ef21c7 100644
--- a/zjit/src/codegen_tests.rs
+++ b/zjit/src/codegen_tests.rs
@@ -2842,6 +2842,38 @@ fn test_fixnum_mod_negative() {
assert_snapshot!(assert_compiles("[test(-7, 3), test(7, -3), test(-7, -3)]"), @"[2, -2, -1]");
}
+#[test]
+fn test_fixnum_aref_constant_index() {
+ eval("
+ def test(a) = a[12]
+ test(4096) # profile opt_aref
+ ");
+ assert_contains_opcode("test", YARVINSN_opt_aref);
+ assert_snapshot!(assert_compiles("[test(4096), test(4095), test(0), test(-1), test(-4096)]"), @"[1, 0, 0, 1, 1]");
+}
+
+#[test]
+fn test_fixnum_aref_constant_index_beyond_fixnum_width() {
+ // An index beyond the fixnum width is not strength-reduced; FixnumAref handles it
+ eval("
+ def test(a) = a[100]
+ test(1) # profile opt_aref
+ ");
+ assert_contains_opcode("test", YARVINSN_opt_aref);
+ assert_snapshot!(assert_compiles("[test(1), test(-1), test(4611686018427387903), test(-4611686018427387904)]"), @"[0, 1, 0, 1]");
+}
+
+#[test]
+fn test_fixnum_aref_constant_index_bignum_receiver() {
+ // A Bignum receiver fails the Fixnum guard and side-exits to the correct result
+ eval("
+ def test(a) = a[1]
+ test(5) # profile opt_aref
+ ");
+ assert_contains_opcode("test", YARVINSN_opt_aref);
+ assert_snapshot!(assert_compiles_allowing_exits("[test(5), test(2**100 + 2)]"), @"[0, 1]");
+}
+
#[test]
fn test_fixnum_mod_by_zero() {
eval("
diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs
index f2a62456bc8279..43bda091ae2996 100644
--- a/zjit/src/cruby_methods.rs
+++ b/zjit/src/cruby_methods.rs
@@ -800,6 +800,24 @@ fn inline_integer_rshift(fun: &mut hir::Function, block: hir::BlockId, recv: hir
fn inline_integer_aref(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option {
let &[index] = args else { return None; };
+
+ // Optimize a compile-time constant index that fits in the Fixnum payload as a bit test.
+ // A Fixnum payload is VALUE_BITS - 1 bits wide (one bit is the tag), so its highest
+ // (sign) bit is at index VALUE_BITS - 2.
+ const FIXNUM_SIGN_BIT_INDEX: i64 = VALUE_BITS as i64 - 2;
+ if fun.likely_a(recv, types::Fixnum, state) {
+ if let Some(index_value) = fun.type_of(index).fixnum_value() {
+ if (0..=FIXNUM_SIGN_BIT_INDEX).contains(&index_value) {
+ // Optimize `recv[i]` into `(recv >> i) & 1`.
+ let recv = fun.coerce_to(block, recv, types::Fixnum, state);
+ let shift = fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(VALUE::fixnum_from_usize(index_value as usize)) });
+ let shifted = fun.push_insn(block, hir::Insn::FixnumRShift { left: recv, right: shift });
+ let mask = fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(VALUE::fixnum_from_usize(1)) });
+ return Some(fun.push_insn(block, hir::Insn::FixnumAnd { left: shifted, right: mask }));
+ }
+ }
+ }
+ // Use a C call (FixnumAref) for any other (e.g. non-constant) index.
if fun.likely_a(recv, types::Fixnum, state) && fun.likely_a(index, types::Fixnum, state) {
let recv = fun.coerce_to(block, recv, types::Fixnum, state);
let index = fun.coerce_to(block, index, types::Fixnum, state);
diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs
index 85b887983606b0..9cc439834b07f0 100644
--- a/zjit/src/hir/opt_tests.rs
+++ b/zjit/src/hir/opt_tests.rs
@@ -2064,6 +2064,67 @@ mod hir_opt_tests {
");
}
+ #[test]
+ fn integer_aref_with_constant_index_strength_reduced() {
+ eval("
+ def test(a) = a[12]
+ test(4096)
+ ");
+ assert_snapshot!(hir_string("test"), @"
+ fn test@:2:
+ bb1():
+ EntryPoint interpreter
+ v1:BasicObject = LoadSelf
+ v2:CPtr = LoadSP
+ v3:BasicObject = LoadField v2, :a@0x1000
+ Jump bb3(v1, v3)
+ bb2():
+ EntryPoint JIT(0)
+ v6:BasicObject = LoadArg :self@0
+ v7:BasicObject = LoadArg :a@1
+ Jump bb3(v6, v7)
+ bb3(v9:BasicObject, v10:BasicObject):
+ v15:Fixnum[12] = Const Value(12)
+ PatchPoint MethodRedefined(Integer@0x1008, []@0x1010, cme:0x1018)
+ v26:Fixnum = GuardType v10, Fixnum recompile
+ v27:Fixnum[12] = Const Value(12)
+ v28:Fixnum = FixnumRShift v26, v27
+ v29:Fixnum[1] = Const Value(1)
+ v30:Fixnum = FixnumAnd v28, v29
+ CheckInterrupts
+ Return v30
+ ");
+ }
+
+ #[test]
+ fn integer_aref_with_constant_index_beyond_fixnum_width() {
+ eval("
+ def test(a) = a[100]
+ test(-1)
+ ");
+ assert_snapshot!(hir_string("test"), @"
+ fn test@:2:
+ bb1():
+ EntryPoint interpreter
+ v1:BasicObject = LoadSelf
+ v2:CPtr = LoadSP
+ v3:BasicObject = LoadField v2, :a@0x1000
+ Jump bb3(v1, v3)
+ bb2():
+ EntryPoint JIT(0)
+ v6:BasicObject = LoadArg :self@0
+ v7:BasicObject = LoadArg :a@1
+ Jump bb3(v6, v7)
+ bb3(v9:BasicObject, v10:BasicObject):
+ v15:Fixnum[100] = Const Value(100)
+ PatchPoint MethodRedefined(Integer@0x1008, []@0x1010, cme:0x1018)
+ v26:Fixnum = GuardType v10, Fixnum recompile
+ v27:Fixnum = FixnumAref v26, v15
+ CheckInterrupts
+ Return v27
+ ");
+ }
+
#[test]
fn elide_fixnum_aref() {
eval("