From 6533b101e3d19a217d8ae2d994bbcec5c4c45307 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 6 Mar 2026 15:57:13 -0500 Subject: [PATCH 01/10] [ruby/prism] Fix precedence of infix operators after command https://github.com/ruby/prism/commit/35470bb90d --- prism/prism.c | 17 ++++++++++++++++- test/prism/errors/command_call_in_2.txt | 4 ++++ test/prism/errors/command_call_in_3.txt | 4 ++++ test/prism/errors/command_call_in_4.txt | 4 ++++ test/prism/errors/command_call_in_5.txt | 4 ++++ test/prism/errors/command_call_in_6.txt | 4 ++++ test/prism/errors/command_call_in_7.txt | 4 ++++ test/prism/errors_test.rb | 4 ---- 8 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 test/prism/errors/command_call_in_2.txt create mode 100644 test/prism/errors/command_call_in_3.txt create mode 100644 test/prism/errors/command_call_in_4.txt create mode 100644 test/prism/errors/command_call_in_5.txt create mode 100644 test/prism/errors/command_call_in_6.txt create mode 100644 test/prism/errors/command_call_in_7.txt diff --git a/prism/prism.c b/prism/prism.c index efe21c5e7e5725..2e2d6bcbd4b3cf 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -21621,7 +21621,7 @@ parse_expression(pm_parser_t *parser, pm_binding_power_t binding_power, bool acc return node; } break; - case PM_CALL_NODE: + case PM_CALL_NODE: { // A do-block can attach to a command-style call // produced by infix operators (e.g., dot-calls like // `obj.method args do end`). @@ -21635,7 +21635,22 @@ parse_expression(pm_parser_t *parser, pm_binding_power_t binding_power, bool acc if (PM_NODE_FLAG_P(node, PM_CALL_NODE_FLAGS_IMPLICIT_ARRAY) && pm_binding_powers[parser->current.type].left > PM_BINDING_POWER_MODIFIER) { return node; } + + // Command-style calls (calls with arguments but without + // parentheses) only accept composition (and/or) and modifier + // (if/unless/etc.) operators. We need to exclude operator calls + // (e.g., a + b) which also satisfy pm_call_node_command_p but + // are not commands. + const pm_call_node_t *cast = (const pm_call_node_t *) node; + if ( + (pm_binding_powers[parser->current.type].left > PM_BINDING_POWER_COMPOSITION) && + (cast->receiver == NULL || cast->call_operator_loc.length > 0) && + pm_call_node_command_p(cast) + ) { + return node; + } break; + } case PM_RESCUE_MODIFIER_NODE: // A rescue modifier whose handler is a one-liner pattern match // (=> or in) produces a statement. That means it cannot be diff --git a/test/prism/errors/command_call_in_2.txt b/test/prism/errors/command_call_in_2.txt new file mode 100644 index 00000000000000..6676b1acba7968 --- /dev/null +++ b/test/prism/errors/command_call_in_2.txt @@ -0,0 +1,4 @@ +a.b x in pattern + ^~ unexpected 'in', expecting end-of-input + ^~ unexpected 'in', ignoring it + diff --git a/test/prism/errors/command_call_in_3.txt b/test/prism/errors/command_call_in_3.txt new file mode 100644 index 00000000000000..6fe026d7d36e1b --- /dev/null +++ b/test/prism/errors/command_call_in_3.txt @@ -0,0 +1,4 @@ +a.b x: in pattern + ^~ unexpected 'in', expecting end-of-input + ^~ unexpected 'in', ignoring it + diff --git a/test/prism/errors/command_call_in_4.txt b/test/prism/errors/command_call_in_4.txt new file mode 100644 index 00000000000000..045afe6498696c --- /dev/null +++ b/test/prism/errors/command_call_in_4.txt @@ -0,0 +1,4 @@ +a.b &x in pattern + ^~ unexpected 'in', expecting end-of-input + ^~ unexpected 'in', ignoring it + diff --git a/test/prism/errors/command_call_in_5.txt b/test/prism/errors/command_call_in_5.txt new file mode 100644 index 00000000000000..be07287f81145b --- /dev/null +++ b/test/prism/errors/command_call_in_5.txt @@ -0,0 +1,4 @@ +a.b *x => pattern + ^~ unexpected '=>', expecting end-of-input + ^~ unexpected '=>', ignoring it + diff --git a/test/prism/errors/command_call_in_6.txt b/test/prism/errors/command_call_in_6.txt new file mode 100644 index 00000000000000..470f323872df0b --- /dev/null +++ b/test/prism/errors/command_call_in_6.txt @@ -0,0 +1,4 @@ +a.b x: => pattern + ^~ unexpected '=>', expecting end-of-input + ^~ unexpected '=>', ignoring it + diff --git a/test/prism/errors/command_call_in_7.txt b/test/prism/errors/command_call_in_7.txt new file mode 100644 index 00000000000000..a8bea912b5d1ce --- /dev/null +++ b/test/prism/errors/command_call_in_7.txt @@ -0,0 +1,4 @@ +a.b &x => pattern + ^~ unexpected '=>', expecting end-of-input + ^~ unexpected '=>', ignoring it + diff --git a/test/prism/errors_test.rb b/test/prism/errors_test.rb index 898f4afb45f20f..c3362eaaf56e5c 100644 --- a/test/prism/errors_test.rb +++ b/test/prism/errors_test.rb @@ -60,10 +60,6 @@ def test_unterminated_empty_string_closing assert_nil statement.closing end - def test_invalid_message_name - assert_equal :"", Prism.parse_statement("+.@foo,+=foo").write_name - end - def test_regexp_encoding_option_mismatch_error # UTF-8 char with ASCII-8BIT modifier result = Prism.parse('/Ȃ/n') From fd9448bc87a51482564ac997e0c1c6e5f36e7927 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 6 Mar 2026 16:01:02 -0500 Subject: [PATCH 02/10] [ruby/prism] Fix not without parentheses binding power https://github.com/ruby/prism/commit/7d21e564ac --- prism/prism.c | 10 ++++++---- test/prism/errors/not_without_parens_assignment.txt | 4 ++++ test/prism/errors/not_without_parens_call.txt | 7 +++++++ test/prism/errors/not_without_parens_command.txt | 4 ++++ test/prism/errors/not_without_parens_command_call.txt | 4 ++++ test/prism/errors/not_without_parens_return.txt | 4 ++++ 6 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 test/prism/errors/not_without_parens_assignment.txt create mode 100644 test/prism/errors/not_without_parens_call.txt create mode 100644 test/prism/errors/not_without_parens_command.txt create mode 100644 test/prism/errors/not_without_parens_command_call.txt create mode 100644 test/prism/errors/not_without_parens_return.txt diff --git a/prism/prism.c b/prism/prism.c index 2e2d6bcbd4b3cf..18aa841ec9bebc 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -19195,10 +19195,12 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b pm_arguments_t arguments = { 0 }; pm_node_t *receiver = NULL; - // If we do not accept a command call, then we also do not accept a - // not without parentheses. In this case we need to reject this - // syntax. - if (!accepts_command_call && !match1(parser, PM_TOKEN_PARENTHESIS_LEFT)) { + // The `not` keyword without parentheses is only valid in contexts + // where it would be parsed as an expression (i.e., at or below + // the `not` binding power level). In other contexts (e.g., method + // arguments, array elements, assignment right-hand sides), + // parentheses are required: `not(x)`. + if (binding_power > PM_BINDING_POWER_NOT && !match1(parser, PM_TOKEN_PARENTHESIS_LEFT)) { if (match1(parser, PM_TOKEN_PARENTHESIS_LEFT_PARENTHESES)) { pm_parser_err(parser, PM_TOKEN_END(parser, &parser->previous), 1, PM_ERR_EXPECT_LPAREN_AFTER_NOT_LPAREN); } else { diff --git a/test/prism/errors/not_without_parens_assignment.txt b/test/prism/errors/not_without_parens_assignment.txt new file mode 100644 index 00000000000000..32d58efedf0642 --- /dev/null +++ b/test/prism/errors/not_without_parens_assignment.txt @@ -0,0 +1,4 @@ +x = not y + ^ expected a `(` after `not` + ^ unexpected local variable or method, expecting end-of-input + diff --git a/test/prism/errors/not_without_parens_call.txt b/test/prism/errors/not_without_parens_call.txt new file mode 100644 index 00000000000000..a77819340090ac --- /dev/null +++ b/test/prism/errors/not_without_parens_call.txt @@ -0,0 +1,7 @@ +foo(not y) + ^ expected a `(` after `not` + ^ unexpected local variable or method; expected a `)` to close the arguments + ^ unexpected local variable or method, expecting end-of-input + ^ unexpected ')', expecting end-of-input + ^ unexpected ')', ignoring it + diff --git a/test/prism/errors/not_without_parens_command.txt b/test/prism/errors/not_without_parens_command.txt new file mode 100644 index 00000000000000..957a06f8f1b92e --- /dev/null +++ b/test/prism/errors/not_without_parens_command.txt @@ -0,0 +1,4 @@ +foo not y + ^ expected a `(` after `not` + ^ unexpected local variable or method, expecting end-of-input + diff --git a/test/prism/errors/not_without_parens_command_call.txt b/test/prism/errors/not_without_parens_command_call.txt new file mode 100644 index 00000000000000..564833c7ded6a8 --- /dev/null +++ b/test/prism/errors/not_without_parens_command_call.txt @@ -0,0 +1,4 @@ +a.b not y + ^ expected a `(` after `not` + ^ unexpected local variable or method, expecting end-of-input + diff --git a/test/prism/errors/not_without_parens_return.txt b/test/prism/errors/not_without_parens_return.txt new file mode 100644 index 00000000000000..1c7edb6ff1497d --- /dev/null +++ b/test/prism/errors/not_without_parens_return.txt @@ -0,0 +1,4 @@ +return not y + ^ expected a `(` after `not` + ^ unexpected local variable or method, expecting end-of-input + From b324803732c8568738a4484be495dcae56997086 Mon Sep 17 00:00:00 2001 From: Nery Campusano Date: Fri, 6 Mar 2026 19:54:53 -0500 Subject: [PATCH 03/10] ZJIT: Add execution counters for JIT-compiled code paths (#16315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds runtime execution tracking for ZJIT-compiled iseqs so we can identify which compiled methods are called most frequently. **Why?** 1. Determining what can be optimized based on how often it's called: knowing call frequency helps prioritize which methods are worth spending time on, and focus improvements on the hottest paths, so ZJIT work has the most impact. 2. Memory eviction: ZJIT has a fixed allocated code region size, and when it fills up it stops compiling new methods. This data could help decide which low-usage methods to drop to make room. **How?** A `HashMap>` is added to `ZJITState` mapping each compiled iseq's location string to a counter. The counter is an `IncrCounterPtr` HIR instruction inserted into the JIT entry block (bb2) during HIR construction in `compile_jit_entry_block`, before the block gets lowered to machine code by `gen_function`. The placement here matters `jit_entry_ptrs` are recorded via `pos_marker` on the `EntryPoint` instruction. By inserting the counter in the HIR it ends up after EntryPoint in the instruction stream, so it actually gets executed on every JIT call. My first attempt emitted the counter directly in `gen_function` before writing the block label, which silently placed it before EntryPoint — making it unreachable since stubs jump to the EntryPoint address, skipping the counter entirely for certain methods. Box is owned by ZJITState for the lifetime of the program so the pointer stays stable across HashMap resizes. Everything is gated behind --zjit-stats, exposed through RubyVM::ZJIT.runtime_stats, and printed as "hottest code paths." Ran zjit-stats on the lobsters [benchmark](https://github.com/ruby/ruby-bench/blob/main/benchmarks/lobsters/benchmark.rb) ```cli Top-20 hottest code paths (20.9% of total 48,150,957): []@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activesupport-8.1.1/lib/active_support/isolated_execution_state.rb:32: 996,342 ( 2.1%) attribute_types@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activemodel-8.1.1/lib/active_model/attribute_registration.rb:38: 796,188 ( 1.7%) block in redefine@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activesupport-8.1.1/lib/active_support/class_attribute.rb:15: 702,587 ( 1.5%) klass@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activerecord-8.1.1/lib/active_record/reflection.rb:423: 645,927 ( 1.3%) _read_attribute@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activerecord-8.1.1/lib/active_record/attribute_methods/read.rb:39: 601,522 ( 1.2%) get_or_default@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/collection/map/non_concurrent_map_backend.rb:111: 477,348 ( 1.0%) fetch@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activerecord-8.1.1/lib/active_record/type/type_map.rb:19: 469,970 ( 1.0%) fetch@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/map.rb:183: 469,970 ( 1.0%) fetch_or_store@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/map.rb:205: 469,970 ( 1.0%) extended_type_map_key@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activerecord-8.1.1/lib/active_record/connection_adapters/abstract_adapter.rb:1173: 469,970 ( 1.0%) type_map@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activerecord-8.1.1/lib/active_record/connection_adapters/abstract_adapter.rb:1179: 469,941 ( 1.0%) fetch@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activerecord-8.1.1/lib/active_record/result.rb:76: 449,631 ( 0.9%) context@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activesupport-8.1.1/lib/active_support/isolated_execution_state.rb:55: 445,689 ( 0.9%) <<@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activerecord-8.1.1/lib/arel/collectors/plain_string.rb:15: 428,350 ( 0.9%) foreign_key@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activerecord-8.1.1/lib/active_record/reflection.rb:559: 405,255 ( 0.8%) period@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activesupport-8.1.1/lib/active_support/time_with_zone.rb:79: 400,182 ( 0.8%) klass@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activerecord-8.1.1/lib/active_record/associations/association.rb:166: 346,231 ( 0.7%) loaded?@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activerecord-8.1.1/lib/active_record/associations/association.rb:82: 344,651 ( 0.7%) get_header@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/rack-2.2.19/lib/rack/request.rb:63: 341,545 ( 0.7%) cast@/Users/nerycampusano/.gem/ruby/ruby-zjit/gems/activemodel-8.1.1/lib/active_model/type/value.rb:58: 326,008 ( 0.7%) ``` --- zjit.rb | 1 + zjit/src/hir.rs | 12 ++++++++++++ zjit/src/state.rs | 9 +++++++++ zjit/src/stats.rs | 11 +++++++++++ 4 files changed, 33 insertions(+) diff --git a/zjit.rb b/zjit.rb index f2cd5330f414ce..82630612749f44 100644 --- a/zjit.rb +++ b/zjit.rb @@ -184,6 +184,7 @@ def stats_string # Show counters independent from exit_* or dynamic_send_* print_counters_with_prefix(prefix: 'not_inlined_cfuncs_', prompt: 'not inlined C methods', buf:, stats:, limit: 20) print_counters_with_prefix(prefix: 'ccall_', prompt: 'calls to C functions from JIT code', buf:, stats:, limit: 20) + print_counters_with_prefix(prefix: 'iseq_calls_count_', prompt: 'most called JIT functions', buf:, stats:, limit: 20) # Don't show not_annotated_cfuncs right now because it mostly duplicates not_inlined_cfuncs # print_counters_with_prefix(prefix: 'not_annotated_cfuncs_', prompt: 'not annotated C methods', buf:, stats:, limit: 20) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 65c49444243145..200502af52f880 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -4446,6 +4446,15 @@ impl Function { self.push_insn(block, Insn::IncrCounterPtr { counter_ptr }); } + fn count_iseq_calls(&mut self, block: BlockId) { + let iseq_name = iseq_get_location(self.iseq, 0); + let access_counter_ptrs = crate::state::ZJITState::get_iseq_calls_count_pointers(); + let counter_ptr = access_counter_ptrs.entry(iseq_name.to_string()).or_insert_with(|| Box::new(0)); + let counter_ptr: &mut u64 = counter_ptr.as_mut(); + + self.push_insn(block, Insn::IncrCounterPtr { counter_ptr }); + } + fn count_not_annotated_cfunc(&mut self, block: BlockId, cme: *const rb_callable_method_entry_t) { let owner = unsafe { (*cme).owner }; let called_id = unsafe { (*cme).called_id }; @@ -8050,6 +8059,9 @@ fn compile_jit_entry_block(fun: &mut Function, jit_entry_idx: usize, target_bloc // Prepare entry_state with basic block params let (self_param, entry_state) = compile_jit_entry_state(fun, jit_entry_block, jit_entry_idx); + if get_option!(stats) { + fun.count_iseq_calls(jit_entry_block); + } // Jump to target_block fun.push_insn(jit_entry_block, Insn::Jump(BranchEdge { target: target_block, args: entry_state.as_args(self_param) })); } diff --git a/zjit/src/state.rs b/zjit/src/state.rs index a7851b2607132a..df8239699ff5e2 100644 --- a/zjit/src/state.rs +++ b/zjit/src/state.rs @@ -62,6 +62,9 @@ pub struct ZJITState { /// Counter pointers for all calls to any kind of C function from JIT code ccall_counter_pointers: HashMap>, + /// Counter pointers for access counts of ISEQs accessed by JIT code + iseq_calls_count_pointers: HashMap>, + /// Locations of side exists within generated code exit_locations: Option, } @@ -139,6 +142,7 @@ impl ZJITState { full_frame_cfunc_counter_pointers: HashMap::new(), not_annotated_frame_cfunc_counter_pointers: HashMap::new(), ccall_counter_pointers: HashMap::new(), + iseq_calls_count_pointers: HashMap::new(), exit_locations, }; unsafe { ZJIT_STATE = Enabled(zjit_state); } @@ -224,6 +228,11 @@ impl ZJITState { &mut ZJITState::get_instance().ccall_counter_pointers } + /// Get a mutable reference to iseq access count pointers + pub fn get_iseq_calls_count_pointers() -> &'static mut HashMap> { + &mut ZJITState::get_instance().iseq_calls_count_pointers + } + /// Was --zjit-save-compiled-iseqs specified? pub fn should_log_compiled_iseqs() -> bool { get_option!(log_compiled_iseqs).is_some() diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 9d832420a6e776..db204f032f9340 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -772,6 +772,10 @@ pub extern "C" fn rb_zjit_reset_stats_bang(_ec: EcPtr, _self: VALUE) -> VALUE { ZJITState::get_ccall_counter_pointers().iter_mut() .for_each(|b| { **(b.1) = 0; }); + // Reset iseq call counters + ZJITState::get_iseq_calls_count_pointers().iter_mut() + .for_each(|b| { **(b.1) = 0; }); + Qnil } @@ -937,6 +941,13 @@ pub extern "C" fn rb_zjit_stats(_ec: EcPtr, _self: VALUE, target_key: VALUE) -> set_stat_usize!(hash, &key_string, **counter); } + // Set iseq access counters + let iseq_access_counts = ZJITState::get_iseq_calls_count_pointers(); + for (iseq_name, counter) in iseq_access_counts.iter() { + let key_string = format!("iseq_calls_count_{iseq_name}"); + set_stat_usize!(hash, &key_string, **counter); + } + hash } From 55694ad7efc3f8dc6d5c7aefa60ded4c303ed6cf Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 7 Mar 2026 10:39:46 +0900 Subject: [PATCH 04/10] [Bug #21945] Correctly handle `and?` and similar --- parse.y | 3 +++ test/ripper/test_lexer.rb | 52 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/parse.y b/parse.y index b91b141cc65375..bcff7918bfa4c3 100644 --- a/parse.y +++ b/parse.y @@ -6978,6 +6978,9 @@ peek_word_at(struct parser_params *p, const char *str, size_t len, int at) if (lex_eol_ptr_n_p(p, ptr, len-1)) return false; if (memcmp(ptr, str, len)) return false; if (lex_eol_ptr_n_p(p, ptr, len)) return true; + switch (ptr[len]) { + case '!': case '?': return false; + } return !is_identchar(p, ptr+len, p->lex.pend, p->enc); } diff --git a/test/ripper/test_lexer.rb b/test/ripper/test_lexer.rb index 7a2c22ff2d1dcb..4bc6fd7ced05df 100644 --- a/test/ripper/test_lexer.rb +++ b/test/ripper/test_lexer.rb @@ -586,6 +586,58 @@ def test_spaces_at_eof assert_lexer(expected, code) end + def test_fluent_and + code = "foo\n" "and" + expected = [ + [[1, 0], :on_ident, "foo", state(:EXPR_CMDARG)], + [[1, 3], :on_ignored_nl, "\n", state(:EXPR_CMDARG)], + [[2, 0], :on_kw, "and", state(:EXPR_BEG)], + ] + assert_lexer(expected, code) + + code = "foo\n" "and?" + expected = [ + [[1, 0], :on_ident, "foo", state(:EXPR_CMDARG)], + [[1, 3], :on_nl, "\n", state(:EXPR_BEG)], + [[2, 0], :on_ident, "and?", state(:EXPR_CMDARG)], + ] + assert_lexer(expected, code) + + code = "foo\n" "and!" + expected = [ + [[1, 0], :on_ident, "foo", state(:EXPR_CMDARG)], + [[1, 3], :on_nl, "\n", state(:EXPR_BEG)], + [[2, 0], :on_ident, "and!", state(:EXPR_CMDARG)], + ] + assert_lexer(expected, code) + end + + def test_fluent_or + code = "foo\n" "or" + expected = [ + [[1, 0], :on_ident, "foo", state(:EXPR_CMDARG)], + [[1, 3], :on_ignored_nl, "\n", state(:EXPR_CMDARG)], + [[2, 0], :on_kw, "or", state(:EXPR_BEG)], + ] + assert_lexer(expected, code) + + code = "foo\n" "or?" + expected = [ + [[1, 0], :on_ident, "foo", state(:EXPR_CMDARG)], + [[1, 3], :on_nl, "\n", state(:EXPR_BEG)], + [[2, 0], :on_ident, "or?", state(:EXPR_CMDARG)], + ] + assert_lexer(expected, code) + + code = "foo\n" "or!" + expected = [ + [[1, 0], :on_ident, "foo", state(:EXPR_CMDARG)], + [[1, 3], :on_nl, "\n", state(:EXPR_BEG)], + [[2, 0], :on_ident, "or!", state(:EXPR_CMDARG)], + ] + assert_lexer(expected, code) + end + def assert_lexer(expected, code) assert_equal(code, Ripper.tokenize(code).join("")) assert_equal(expected, result = Ripper.lex(code), From 38c9f14b18942d03594781bbc3d5498397b4d03c Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 7 Mar 2026 13:04:37 +0900 Subject: [PATCH 05/10] vcs.rb: Extract `parse_iso_date` method --- tool/lib/vcs.rb | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tool/lib/vcs.rb b/tool/lib/vcs.rb index ce545ec368eef9..8b06eb03645898 100644 --- a/tool/lib/vcs.rb +++ b/tool/lib/vcs.rb @@ -169,19 +169,7 @@ def get_revisions(path) ) last or raise VCS::NotFoundError, "last revision not found" changed or raise VCS::NotFoundError, "changed revision not found" - if modified - /\A(\d+)-(\d+)-(\d+)\D(\d+):(\d+):(\d+(?:\.\d+)?)\s*(?:Z|([-+]\d\d)(\d\d))\z/ =~ modified or - raise "unknown time format - #{modified}" - match = $~[1..6].map { |x| x.to_i } - off = $7 ? "#{$7}:#{$8}" : "+00:00" - match << off - begin - modified = Time.new(*match) - rescue ArgumentError - modified = Time.utc(*$~[1..6]) + $7.to_i * 3600 + $8.to_i * 60 - end - modified = modified.getlocal(@zone) - end + modified &&= parse_iso_date(modified) return last, changed, modified, *rest end @@ -210,6 +198,20 @@ def relative_to(path) end end + def parse_iso_date(date) + /\A(\d+)-(\d+)-(\d+)\D(\d+):(\d+):(\d+(?:\.\d+)?)\s*(?:Z|([-+]\d\d)(\d\d))\z/ =~ date or + raise "unknown time format - #{date}" + match = $~[1..6].map { |x| x.to_i } + off = $7 ? "#{$7}:#{$8}" : "+00:00" + match << off + begin + date = Time.new(*match) + rescue ArgumentError + date = Time.utc(*$~[1..6]) + $7.to_i * 3600 + $8.to_i * 60 + end + date.getlocal(@zone) + end + def after_export(dir) FileUtils.rm_rf(Dir.glob("#{dir}/.git*")) FileUtils.rm_rf(Dir.glob("#{dir}/.mailmap")) From 9fd8dd4af94bc30170704bc7cbf635bfa99e1209 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 7 Mar 2026 13:04:51 +0900 Subject: [PATCH 06/10] vcs.rb: Make `relative_to` accept the base directory name optionally --- tool/lib/vcs.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tool/lib/vcs.rb b/tool/lib/vcs.rb index 8b06eb03645898..1f55d63c181f97 100644 --- a/tool/lib/vcs.rb +++ b/tool/lib/vcs.rb @@ -178,9 +178,9 @@ def modified(path) modified end - def relative_to(path) + def relative_to(path, srcdir = @srcdir) if path - srcdir = File.realpath(@srcdir) + srcdir = File.realpath(srcdir || @srcdir) path = File.realdirpath(path) list1 = srcdir.split(%r{/}) list2 = path.split(%r{/}) From f3e1dfc8f61a7b0f2053d259cf7e037020014470 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 7 Mar 2026 13:05:47 +0900 Subject: [PATCH 07/10] vcs.rb: Add `VCS::GIT#author_date` method Returns the author date of the latest commit for the path. --- tool/lib/vcs.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tool/lib/vcs.rb b/tool/lib/vcs.rb index 1f55d63c181f97..d6374f9de05946 100644 --- a/tool/lib/vcs.rb +++ b/tool/lib/vcs.rb @@ -364,6 +364,11 @@ def _get_revisions(path, srcdir = nil) [last, changed, modified, branch, title] end + def author_date(path, srcdir = @srcdir) + log = cmd_read_at(srcdir, [[COMMAND, 'log', '-n1', '--pretty=%at', path]]) + Time.at(log.to_i, in: @zone) + end + def self.revision_name(rev) short_revision(rev) end From 12038f1997d40805d092340994c128d63e25a7b6 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 7 Mar 2026 13:12:15 +0900 Subject: [PATCH 08/10] [DOC] Update the date in man pages by the author date Prefer the date authored the contents over the merged date for the embedded dates. --- common.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.mk b/common.mk index 963af3987b55ef..349d2a87e30906 100644 --- a/common.mk +++ b/common.mk @@ -1900,7 +1900,7 @@ sudo-precheck: PHONY update-man-date: PHONY $(Q) $(BASERUBY) -I"$(tooldir)/lib" -rvcs -i -p \ -e 'BEGIN{@vcs=VCS.detect(ARGV.shift)}' \ - -e '$$_.sub!(/^(\.Dd ).*/){$$1+@vcs.modified(ARGF.path).strftime("%B %d, %Y")}' \ + -e '$$_.sub!(/^(\.Dd ).*/){$$1+@vcs.author_date(@vcs.relative_to(ARGF.path)).strftime("%B %d, %Y")}' \ "$(srcdir)" "$(srcdir)"/man/*.1 .PHONY: ChangeLog From 55df8dc063df1c749dbe07f78158f85a0ae47a99 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 7 Mar 2026 13:43:19 +0900 Subject: [PATCH 09/10] [DOC] Update the date in man pages if changed --- .github/workflows/check_misc.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 32cbcb53c8a7cf..a75b564654856e 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -52,6 +52,16 @@ jobs: # Skip 'push' events because post_push.yml fixes them on push if: ${{ github.repository == 'ruby/ruby' && startsWith(github.event_name, 'pull') }} + - name: Check if date in man pages is up-to-date + run: | + git fetch origin --depth=1 "${GITHUB_OLD_SHA}" + git diff --exit-code --name-only "${GITHUB_OLD_SHA}" HEAD -- man || + make V=1 GIT=git BASERUBY=ruby update-man-date + git diff --color --no-ext-diff --ignore-submodules --exit-code -- man + env: + GITHUB_OLD_SHA: ${{ github.event.pull_request.base.sha }} + if: ${{ startsWith(github.event_name, 'pull') }} + - name: Check for bash specific substitution in configure.ac run: | git grep -n '\${[A-Za-z_0-9]*/' -- configure.ac && exit 1 || : From 66e61d0cffca501f335175c6ca4948d535a0e86d Mon Sep 17 00:00:00 2001 From: git Date: Sat, 7 Mar 2026 07:00:25 +0000 Subject: [PATCH 10/10] [DOC] Update bundled gems list at 55df8dc063df1c749dbe07f78158f8 --- NEWS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 79b5cc9ff87330..624440abc91f20 100644 --- a/NEWS.md +++ b/NEWS.md @@ -65,7 +65,7 @@ releases. * RubyGems 4.1.0.dev * bundler 4.1.0.dev * json 2.19.0 - * 2.18.0 to [v2.18.1][json-v2.18.1] + * 2.18.0 to [v2.18.1][json-v2.18.1], [v2.19.0][json-v2.19.0] * openssl 4.0.1 * 4.0.0 to [v4.0.1][openssl-v4.0.1] * prism 1.9.0 @@ -134,6 +134,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #21390]: https://bugs.ruby-lang.org/issues/21390 [Feature #21785]: https://bugs.ruby-lang.org/issues/21785 [json-v2.18.1]: https://github.com/ruby/json/releases/tag/v2.18.1 +[json-v2.19.0]: https://github.com/ruby/json/releases/tag/v2.19.0 [openssl-v4.0.1]: https://github.com/ruby/openssl/releases/tag/v4.0.1 [prism-v1.9.0]: https://github.com/ruby/prism/releases/tag/v1.9.0 [resolv-v0.7.1]: https://github.com/ruby/resolv/releases/tag/v0.7.1