From 8b7b58735aeed748764397875210765a4cd63661 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 26 Aug 2025 06:55:45 -0700 Subject: [PATCH 01/10] ZJIT: Side-exit on unknown instructions (#14212) Don't abort the entire compilation. Fix https://github.com/Shopify/ruby/issues/700 --- zjit/src/codegen.rs | 51 ++++++++++++++++++++++++--------------------- zjit/src/hir.rs | 15 +++++++------ 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index ed0c52a91169a0..51c54846ba8cf5 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -281,11 +281,15 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function) -> Optio // Compile all instructions for &insn_id in block.insns() { let insn = function.find(insn_id); - if gen_insn(cb, &mut jit, &mut asm, function, insn_id, &insn).is_none() { - debug!("Failed to compile insn: {insn_id} {insn}"); + if let Err(last_snapshot) = gen_insn(cb, &mut jit, &mut asm, function, insn_id, &insn) { + debug!("ZJIT: gen_function: Failed to compile insn: {insn_id} {insn}. Generating side-exit."); incr_counter!(failed_gen_insn); - return None; - } + gen_side_exit(&mut jit, &mut asm, &SideExitReason::UnhandledInstruction(insn_id), &function.frame_state(last_snapshot)); + // Don't bother generating code after a side-exit. We won't run it. + // TODO(max): Generate ud2 or equivalent. + break; + }; + // It's fine; we generated the instruction } // Make sure the last patch point has enough space to insert a jump asm.pad_patch_point(); @@ -316,7 +320,7 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function) -> Optio } /// Compile an instruction -fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, function: &Function, insn_id: InsnId, insn: &Insn) -> Option<()> { +fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, function: &Function, insn_id: InsnId, insn: &Insn) -> Result<(), InsnId> { // Convert InsnId to lir::Opnd macro_rules! opnd { ($insn_id:ident) => { @@ -334,7 +338,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio macro_rules! no_output { ($call:expr) => { - { let () = $call; return Some(()); } + { let () = $call; return Ok(()); } }; } @@ -344,6 +348,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio let out_opnd = match insn { Insn::Const { val: Const::Value(val) } => gen_const(*val), + Insn::Const { .. } => panic!("Unexpected Const in gen_insn: {insn}"), Insn::NewArray { elements, state } => gen_new_array(asm, opnds!(elements), &function.frame_state(*state)), Insn::NewHash { elements, state } => gen_new_hash(jit, asm, elements, &function.frame_state(*state)), Insn::NewRange { low, high, flag, state } => gen_new_range(jit, asm, opnd!(low), opnd!(high), *flag, &function.frame_state(*state)), @@ -351,12 +356,12 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::StringCopy { val, chilled, state } => gen_string_copy(asm, opnd!(val), *chilled, &function.frame_state(*state)), // concatstrings shouldn't have 0 strings // If it happens we abort the compilation for now - Insn::StringConcat { strings, .. } if strings.is_empty() => return None, + Insn::StringConcat { strings, state, .. } if strings.is_empty() => return Err(*state), Insn::StringConcat { strings, state } => gen_string_concat(jit, asm, opnds!(strings), &function.frame_state(*state)), Insn::StringIntern { val, state } => gen_intern(asm, opnd!(val), &function.frame_state(*state)), Insn::ToRegexp { opt, values, state } => gen_toregexp(jit, asm, *opt, opnds!(values), &function.frame_state(*state)), Insn::Param { idx } => unreachable!("block.insns should not have Insn::Param({idx})"), - Insn::Snapshot { .. } => return Some(()), // we don't need to do anything for this instruction at the moment + Insn::Snapshot { .. } => return Ok(()), // we don't need to do anything for this instruction at the moment Insn::Jump(branch) => no_output!(gen_jump(jit, asm, branch)), Insn::IfTrue { val, target } => no_output!(gen_if_true(jit, asm, opnd!(val), target)), Insn::IfFalse { val, target } => no_output!(gen_if_false(jit, asm, opnd!(val), target)), @@ -367,7 +372,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::SendWithoutBlockDirect { cme, iseq, self_val, args, state, .. } => gen_send_without_block_direct(cb, jit, asm, *cme, *iseq, opnd!(self_val), opnds!(args), &function.frame_state(*state)), // Ensure we have enough room fit ec, self, and arguments // TODO remove this check when we have stack args (we can use Time.new to test it) - Insn::InvokeBuiltin { bf, .. } if bf.argc + 2 > (C_ARG_OPNDS.len() as i32) => return None, + Insn::InvokeBuiltin { bf, state, .. } if bf.argc + 2 > (C_ARG_OPNDS.len() as i32) => return Err(*state), Insn::InvokeBuiltin { bf, args, state, .. } => gen_invokebuiltin(jit, asm, &function.frame_state(*state), bf, opnds!(args)), Insn::Return { val } => no_output!(gen_return(asm, opnd!(val))), Insn::FixnumAdd { left, right, state } => gen_fixnum_add(jit, asm, opnd!(left), opnd!(right), &function.frame_state(*state)), @@ -403,22 +408,20 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::IncrCounter(counter) => no_output!(gen_incr_counter(asm, counter)), Insn::ObjToString { val, cd, state, .. } => gen_objtostring(jit, asm, opnd!(val), *cd, &function.frame_state(*state)), &Insn::CheckInterrupts { state } => no_output!(gen_check_interrupts(jit, asm, &function.frame_state(state))), - Insn::ArrayExtend { .. } - | Insn::ArrayMax { .. } - | Insn::ArrayPush { .. } - | Insn::DefinedIvar { .. } - | Insn::FixnumDiv { .. } - | Insn::FixnumMod { .. } - | Insn::HashDup { .. } - | Insn::Send { .. } - | Insn::Throw { .. } - | Insn::ToArray { .. } - | Insn::ToNewArray { .. } - | Insn::Const { .. } + &Insn::ArrayExtend { state, .. } + | &Insn::ArrayMax { state, .. } + | &Insn::ArrayPush { state, .. } + | &Insn::DefinedIvar { state, .. } + | &Insn::FixnumDiv { state, .. } + | &Insn::FixnumMod { state, .. } + | &Insn::HashDup { state, .. } + | &Insn::Send { state, .. } + | &Insn::Throw { state, .. } + | &Insn::ToArray { state, .. } + | &Insn::ToNewArray { state, .. } => { - debug!("ZJIT: gen_function: unexpected insn {insn}"); incr_counter!(failed_gen_insn_unexpected); - return None; + return Err(state); } }; @@ -427,7 +430,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio // If the instruction has an output, remember it in jit.opnds jit.opnds[insn_id.0] = Some(out_opnd); - Some(()) + Ok(()) } /// Gets the EP of the ISeq of the containing method, or "local level". diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 15836b8c447444..5cbedece682f70 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -431,6 +431,7 @@ pub enum SideExitReason { UnknownNewarraySend(vm_opt_newarray_send_type), UnknownCallType, UnknownOpcode(u32), + UnhandledInstruction(InsnId), FixnumAddOverflow, FixnumSubOverflow, FixnumMultOverflow, @@ -567,7 +568,7 @@ pub enum Insn { /// Control flow instructions Return { val: InsnId }, /// Non-local control flow. See the throw YARV instruction - Throw { throw_state: u32, val: InsnId }, + Throw { throw_state: u32, val: InsnId, state: InsnId }, /// Fixnum +, -, *, /, %, ==, !=, <, <=, >, >=, &, | FixnumAdd { left: InsnId, right: InsnId, state: InsnId }, @@ -854,7 +855,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::AnyToString { val, str, .. } => { write!(f, "AnyToString {val}, str: {str}") }, Insn::SideExit { reason, .. } => write!(f, "SideExit {reason}"), Insn::PutSpecialObject { value_type } => write!(f, "PutSpecialObject {value_type}"), - Insn::Throw { throw_state, val } => { + Insn::Throw { throw_state, val, .. } => { write!(f, "Throw ")?; match throw_state & VM_THROW_STATE_MASK { RUBY_TAG_NONE => write!(f, "TAG_NONE"), @@ -1218,7 +1219,7 @@ impl Function { } }, &Return { val } => Return { val: find!(val) }, - &Throw { throw_state, val } => Throw { throw_state, val: find!(val) }, + &Throw { throw_state, val, state } => Throw { throw_state, val: find!(val), state }, &StringCopy { val, chilled, state } => StringCopy { val: find!(val), chilled, state }, &StringIntern { val, state } => StringIntern { val: find!(val), state: find!(state) }, &StringConcat { ref strings, state } => StringConcat { strings: find_vec!(strings), state: find!(state) }, @@ -1992,7 +1993,6 @@ impl Function { worklist.push_back(state); } | &Insn::Return { val } - | &Insn::Throw { val, .. } | &Insn::Test { val } | &Insn::SetLocal { val, .. } | &Insn::IsNil { val } => @@ -2040,7 +2040,9 @@ impl Function { worklist.push_back(val); worklist.extend(args); } - &Insn::ArrayDup { val, state } | &Insn::HashDup { val, state } => { + &Insn::ArrayDup { val, state } + | &Insn::Throw { val, state, .. } + | &Insn::HashDup { val, state } => { worklist.push_back(val); worklist.push_back(state); } @@ -3261,7 +3263,8 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { break; // Don't enqueue the next block as a successor } YARVINSN_throw => { - fun.push_insn(block, Insn::Throw { throw_state: get_arg(pc, 0).as_u32(), val: state.stack_pop()? }); + let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + fun.push_insn(block, Insn::Throw { throw_state: get_arg(pc, 0).as_u32(), val: state.stack_pop()?, state: exit_id }); break; // Don't enqueue the next block as a successor } From 1928df4d334f9ea794c21c9ef39cb9c3cbb99ec3 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 26 Aug 2025 09:53:20 -0400 Subject: [PATCH 02/10] [DOC] Improve documentation of Kernel#Pathname --- pathname_builtin.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 081b82ba9a6a51..4a0ecb84804996 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1163,9 +1163,7 @@ class Pathname end module Kernel - # create a pathname object. - # - # This method is available since 1.8.5. + # Creates a Pathname object. def Pathname(path) # :doc: return path if Pathname === path Pathname.new(path) From da6198de8f27454fa18d6d7f9bcbaf36a3476255 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 26 Aug 2025 10:01:00 -0400 Subject: [PATCH 03/10] [DOC] Document constants in Pathname --- pathname_builtin.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 4a0ecb84804996..486e49d0920212 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -311,7 +311,9 @@ def sub_ext(repl) end if File::ALT_SEPARATOR + # Separator list string. SEPARATOR_LIST = "#{Regexp.quote File::ALT_SEPARATOR}#{Regexp.quote File::SEPARATOR}" + # Regexp that matches a separator. SEPARATOR_PAT = /[#{SEPARATOR_LIST}]/ else SEPARATOR_LIST = "#{Regexp.quote File::SEPARATOR}" @@ -319,6 +321,7 @@ def sub_ext(repl) end if File.dirname('A:') == 'A:.' # DOSish drive letter + # Regexp that matches an absoltute path. ABSOLUTE_PATH = /\A(?:[A-Za-z]:|#{SEPARATOR_PAT})/ else ABSOLUTE_PATH = /\A#{SEPARATOR_PAT}/ From 238aaa4cda14add04f7ecb4ff6fc52719589e89d Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 26 Aug 2025 10:03:58 -0400 Subject: [PATCH 04/10] [DOC] Document undocumented methods in ZJIT --- zjit.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zjit.rb b/zjit.rb index e07bccb132f1d0..3da68b380d7990 100644 --- a/zjit.rb +++ b/zjit.rb @@ -61,6 +61,7 @@ def stats_string buf end + # Outputs counters into +buf+. def print_counters(keys, buf:, stats:) left_pad = keys.map(&:size).max + 1 keys.each do |key| @@ -82,6 +83,7 @@ def print_counters(keys, buf:, stats:) end end + # Similar to #print_counters but only includes keys that start with +prefix+. def print_counters_with_prefix(buf:, stats:, prefix:, prompt:) keys = stats.keys.select { |key| key.start_with?(prefix) && stats[key] > 0 } unless keys.empty? From 63a99113cdc40ec488142f99f0c4174ad13e6e60 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 26 Aug 2025 10:17:12 -0700 Subject: [PATCH 05/10] ZJIT: Avoid documenting internal methods A different doc-test fix from 238aaa4cda14add04f7ecb4ff6fc52719589e89d. They're not supposed to be public in the first place. --- zjit.rb | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/zjit.rb b/zjit.rb index 3da68b380d7990..b20c110046fd59 100644 --- a/zjit.rb +++ b/zjit.rb @@ -61,7 +61,14 @@ def stats_string buf end - # Outputs counters into +buf+. + # Assert that any future ZJIT compilation will return a function pointer + def assert_compiles # :nodoc: + Primitive.rb_zjit_assert_compiles + end + + # :stopdoc: + private + def print_counters(keys, buf:, stats:) left_pad = keys.map(&:size).max + 1 keys.each do |key| @@ -83,7 +90,6 @@ def print_counters(keys, buf:, stats:) end end - # Similar to #print_counters but only includes keys that start with +prefix+. def print_counters_with_prefix(buf:, stats:, prefix:, prompt:) keys = stats.keys.select { |key| key.start_with?(prefix) && stats[key] > 0 } unless keys.empty? @@ -92,14 +98,6 @@ def print_counters_with_prefix(buf:, stats:, prefix:, prompt:) end end - # Assert that any future ZJIT compilation will return a function pointer - def assert_compiles # :nodoc: - Primitive.rb_zjit_assert_compiles - end - - # :stopdoc: - private - def number_with_delimiter(number) s = number.to_s i = s.index('.') || s.size From d113bc5a6d5eb553f076a2f5dfdc974e63ea78da Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 26 Aug 2025 10:27:25 -0700 Subject: [PATCH 06/10] Fix uninitialized next_shape_id (#14348) --- variable.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variable.c b/variable.c index e38af116ec679f..b5620af27b8657 100644 --- a/variable.c +++ b/variable.c @@ -4490,13 +4490,13 @@ class_fields_ivar_set(VALUE klass, VALUE fields_obj, ID id, VALUE val, bool conc fields_obj = original_fields_obj ? original_fields_obj : rb_imemo_fields_new(klass, 1); shape_id_t current_shape_id = RBASIC_SHAPE_ID(fields_obj); - + shape_id_t next_shape_id = current_shape_id; // for too_complex if (UNLIKELY(rb_shape_too_complex_p(current_shape_id))) { goto too_complex; } bool new_ivar; - shape_id_t next_shape_id = generic_shape_ivar(fields_obj, id, &new_ivar); + next_shape_id = generic_shape_ivar(fields_obj, id, &new_ivar); if (UNLIKELY(rb_shape_too_complex_p(next_shape_id))) { fields_obj = imemo_fields_complex_from_obj(klass, fields_obj, next_shape_id); From bb9116281ba8bc9e9d1bf0c72e75bea79cf0f89b Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Tue, 26 Aug 2025 13:23:33 -0500 Subject: [PATCH 07/10] [DOC] Tweaks for String#ljust --- doc/string/ljust.rdoc | 14 ++++++-------- string.c | 4 +--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/doc/string/ljust.rdoc b/doc/string/ljust.rdoc index 8e23c1fc8fef16..f37c0b3151176d 100644 --- a/doc/string/ljust.rdoc +++ b/doc/string/ljust.rdoc @@ -1,16 +1,14 @@ -Returns a left-justified copy of +self+. - -If integer argument +size+ is greater than the size (in characters) of +self+, -returns a new string of length +size+ that is a copy of +self+, -left justified and padded on the right with +pad_string+: +Returns a copy of +self+, left-justified and, if necessary, right-padded with the +pad_string+: 'hello'.ljust(10) # => "hello " ' hello'.ljust(10) # => " hello " 'hello'.ljust(10, 'ab') # => "helloababa" 'тест'.ljust(10) # => "тест " - 'こんにちは'.ljust(10) # => "こんにちは " + 'こんにちは'.ljust(10) # => "こんにちは " -If +size+ is not greater than the size of +self+, returns a copy of +self+: +If width <= self.length, returns a copy of +self+: 'hello'.ljust(5) # => "hello" - 'hello'.ljust(1) # => "hello" + 'hello'.ljust(1) # => "hello" # Does not truncate to width. + +Related: see {Converting to New String}[rdoc-ref:String@Converting+to+New+String]. diff --git a/string.c b/string.c index e022831ba5c8d5..709ada47dbcb19 100644 --- a/string.c +++ b/string.c @@ -11075,12 +11075,10 @@ rb_str_justify(int argc, VALUE *argv, VALUE str, char jflag) /* * call-seq: - * ljust(size, pad_string = ' ') -> new_string + * ljust(width, pad_string = ' ') -> new_string * * :include: doc/string/ljust.rdoc * - * Related: String#rjust, String#center. - * */ static VALUE From dbfd0973d3f2810c6ee490f0bc84a35d6440a3f1 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Tue, 26 Aug 2025 13:27:42 -0500 Subject: [PATCH 08/10] [DOC] Tweaks for String#inspect --- doc/string/inspect.rdoc | 39 +++++++++++++++++++++++++++++++++++++++ string.c | 7 +------ 2 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 doc/string/inspect.rdoc diff --git a/doc/string/inspect.rdoc b/doc/string/inspect.rdoc new file mode 100644 index 00000000000000..828ecf966dd26f --- /dev/null +++ b/doc/string/inspect.rdoc @@ -0,0 +1,39 @@ +Returns a printable version of +self+, enclosed in double-quotes. + +Most printable characters are rendered simply as themselves: + + 'abc'.inspect # => "\"abc\"" + '012'.inspect # => "\"012\"" + ''.inspect # => "\"\"" + "\u000012".inspect # => "\"\\u000012\"" + 'тест'.inspect # => "\"тест\"" + 'こんにちは'.inspect # => "\"こんにちは\"" + +But printable characters double-quote ('"') and backslash and ('\\') are escaped: + + '"'.inspect # => "\"\\\"\"" + '\\'.inspect # => "\"\\\\\"" + +Unprintable characters are the {ASCII characters}[https://en.wikipedia.org/wiki/ASCII] +whose values are in range 0..31, +along with the character whose value is +127+. + +Most of these characters are rendered thus: + + 0.chr.inspect # => "\"\\x00\"" + 1.chr.inspect # => "\"\\x01\"" + 2.chr.inspect # => "\"\\x02\"" + # ... + +A few, however, have special renderings: + + 7.chr.inspect # => "\"\\a\"" # BEL + 8.chr.inspect # => "\"\\b\"" # BS + 9.chr.inspect # => "\"\\t\"" # TAB + 10.chr.inspect # => "\"\\n\"" # LF + 11.chr.inspect # => "\"\\v\"" # VT + 12.chr.inspect # => "\"\\f\"" # FF + 13.chr.inspect # => "\"\\r\"" # CR + 27.chr.inspect # => "\"\\e\"" # ESC + +Related: see {Converting to Non-String}[rdoc-ref:String@Converting+to+Non--5CString]. diff --git a/string.c b/string.c index 709ada47dbcb19..01cfc9c17563cc 100644 --- a/string.c +++ b/string.c @@ -7303,12 +7303,7 @@ rb_str_escape(VALUE str) * call-seq: * inspect -> string * - * Returns a printable version of +self+, enclosed in double-quotes, - * and with special characters escaped: - * - * s = "foo\tbar\tbaz\n" - * s.inspect - * # => "\"foo\\tbar\\tbaz\\n\"" + * :include: doc/string/inspect.rdoc * */ From 6a2964d28cd49c674c0b23958f922ac914253dd1 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Tue, 26 Aug 2025 13:27:54 -0500 Subject: [PATCH 09/10] [DOC] Tweaks for String#intern (#14314) --- doc/string/intern.rdoc | 9 +++++++++ symbol.c | 16 ++-------------- 2 files changed, 11 insertions(+), 14 deletions(-) create mode 100644 doc/string/intern.rdoc diff --git a/doc/string/intern.rdoc b/doc/string/intern.rdoc new file mode 100644 index 00000000000000..1336e4688f7a2e --- /dev/null +++ b/doc/string/intern.rdoc @@ -0,0 +1,9 @@ +Returns the Symbol object derived from +self+, +creating it if it did not already exist: + + 'foo'.intern # => :foo + 'тест'.intern # => :тест + 'こんにちは'.intern # => :こんにちは + +Related: see {Converting to Non-String}[rdoc-ref:String@Converting+to+Non--5CString]. + diff --git a/symbol.c b/symbol.c index ddb0f1556ba019..e8eacd34c2b4b8 100644 --- a/symbol.c +++ b/symbol.c @@ -927,22 +927,10 @@ rb_gc_free_dsymbol(VALUE sym) /* * call-seq: - * str.intern -> symbol - * str.to_sym -> symbol + * intern -> symbol * - * Returns the +Symbol+ corresponding to str, creating the - * symbol if it did not previously exist. See Symbol#id2name. + * :include: doc/string/intern.rdoc * - * "Koala".intern #=> :Koala - * s = 'cat'.to_sym #=> :cat - * s == :cat #=> true - * s = '@cat'.to_sym #=> :@cat - * s == :@cat #=> true - * - * This can also be used to create symbols that cannot be represented using the - * :xxx notation. - * - * 'cat and dog'.to_sym #=> :"cat and dog" */ VALUE From 1e738447f1394d8274a26569f9e38a7e6cea10a4 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Tue, 26 Aug 2025 13:05:11 -0500 Subject: [PATCH 10/10] [DOC] Link fix for String --- doc/string.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/string.rb b/doc/string.rb index 9ed97d49f6fb95..c75d171876a1ea 100644 --- a/doc/string.rb +++ b/doc/string.rb @@ -280,7 +280,8 @@ # If the argument +capture+ is provided and not 0, # it should be either a capture group index (integer) # or a capture group name (String or Symbol); -# the slice is the specified capture (see Regexp@Groups and Captures): +# the slice is the specified capture +# (see {Groups and Captures}[rdoc-ref:Regexp@Groups+and+Captures]): # # s = 'hello there' # s[/[aeiou](.)\1/, 1] # => "l"