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 || : 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 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 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/prism/prism.c b/prism/prism.c index efe21c5e7e5725..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 { @@ -21621,7 +21623,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 +21637,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/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 + 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') 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), diff --git a/tool/lib/vcs.rb b/tool/lib/vcs.rb index ce545ec368eef9..d6374f9de05946 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 @@ -190,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{/}) @@ -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")) @@ -362,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 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 }