diff --git a/doc/yjit/yjit.md b/doc/yjit/yjit.md index 73db7237109322..24aa163e60d299 100644 --- a/doc/yjit/yjit.md +++ b/doc/yjit/yjit.md @@ -14,7 +14,7 @@ This project is open source and falls under the same license as CRuby.

If you're using YJIT in production, please - share your success stories with us! + share your success stories with us!

If you wish to learn more about the approach taken, here are some conference talks and publications: diff --git a/doc/zjit.md b/doc/zjit.md index 3d7ee33abfa438..bb20b9f6924bac 100644 --- a/doc/zjit.md +++ b/doc/zjit.md @@ -162,6 +162,14 @@ A file called `zjit_exits_{pid}.dump` will be created in the same directory as ` stackprof path/to/zjit_exits_{pid}.dump ``` +### Viewing HIR in Iongraph + +Using `--zjit-dump-hir-iongraph` will dump all compiled functions into a directory named `/tmp/zjit-iongraph-{PROCESS_PID}`. Each file will be named `func_{ZJIT_FUNC_NAME}.json`. In order to use them in the Iongraph viewer, you'll need to use `jq` to collate them to a single file. An example invocation of `jq` is shown below for reference. + +`jq --slurp --null-input '.functions=inputs | .version=2' /tmp/zjit-iongraph-{PROCESS_PID}/func*.json > ~/Downloads/ion.json` + +From there, you can use https://mozilla-spidermonkey.github.io/iongraph/ to view your trace. + ### Printing ZJIT Errors `--zjit-debug` prints ZJIT compilation errors and other diagnostics: diff --git a/gc/default/default.c b/gc/default/default.c index 82741458bb8ff3..42561543d1a7c7 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -7592,6 +7592,9 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) enum gc_stat_heap_sym { gc_stat_heap_sym_slot_size, + gc_stat_heap_sym_heap_live_slots, + gc_stat_heap_sym_heap_free_slots, + gc_stat_heap_sym_heap_final_slots, gc_stat_heap_sym_heap_eden_pages, gc_stat_heap_sym_heap_eden_slots, gc_stat_heap_sym_total_allocated_pages, @@ -7610,6 +7613,9 @@ setup_gc_stat_heap_symbols(void) if (gc_stat_heap_symbols[0] == 0) { #define S(s) gc_stat_heap_symbols[gc_stat_heap_sym_##s] = ID2SYM(rb_intern_const(#s)) S(slot_size); + S(heap_live_slots); + S(heap_free_slots); + S(heap_final_slots); S(heap_eden_pages); S(heap_eden_slots); S(total_allocated_pages); @@ -7631,6 +7637,9 @@ stat_one_heap(rb_heap_t *heap, VALUE hash, VALUE key) rb_hash_aset(hash, gc_stat_heap_symbols[gc_stat_heap_sym_##name], SIZET2NUM(attr)); SET(slot_size, heap->slot_size); + SET(heap_live_slots, heap->total_allocated_objects - heap->total_freed_objects - heap->final_slots_count); + SET(heap_free_slots, heap->total_slots - (heap->total_allocated_objects - heap->total_freed_objects)); + SET(heap_final_slots, heap->final_slots_count); SET(heap_eden_pages, heap->total_pages); SET(heap_eden_slots, heap->total_slots); SET(total_allocated_pages, heap->total_allocated_pages); diff --git a/lib/error_highlight/base.rb b/lib/error_highlight/base.rb index 22e0bf0dc39738..bc4a62c9d63508 100644 --- a/lib/error_highlight/base.rb +++ b/lib/error_highlight/base.rb @@ -913,7 +913,7 @@ def prism_spot_constant_path_operator_write # ^^^ def prism_spot_def_for_name location = @node.name_loc - location = location.join(@node.operator_loc) if @node.operator_loc + location = @node.operator_loc.join(location) if @node.operator_loc prism_location(location) end diff --git a/spec/ruby/command_line/dash_0_spec.rb b/spec/ruby/command_line/dash_0_spec.rb index 73c5e29004eb42..2ce4f49b5e4d83 100755 --- a/spec/ruby/command_line/dash_0_spec.rb +++ b/spec/ruby/command_line/dash_0_spec.rb @@ -5,7 +5,7 @@ ruby_exe("puts $/, $-0", options: "-072").should == ":\n:\n" end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "sets $/ and $-0 as a frozen string" do ruby_exe("puts $/.frozen?, $-0.frozen?", options: "-072").should == "true\ntrue\n" end diff --git a/spec/ruby/core/enumerable/to_set_spec.rb b/spec/ruby/core/enumerable/to_set_spec.rb index e0437fea613bd9..e1fcd3a20d0273 100644 --- a/spec/ruby/core/enumerable/to_set_spec.rb +++ b/spec/ruby/core/enumerable/to_set_spec.rb @@ -11,7 +11,7 @@ [1, 2, 3].to_set { |x| x * x }.should == Set[1, 4, 9] end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "instantiates an object of provided as the first argument set class" do set = nil proc{set = [1, 2, 3].to_set(EnumerableSpecs::SetSubclass)}.should complain(/Enumerable#to_set/) @@ -20,7 +20,7 @@ end end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "instantiates an object of provided as the first argument set class" do set = [1, 2, 3].to_set(EnumerableSpecs::SetSubclass) set.should be_kind_of(EnumerableSpecs::SetSubclass) diff --git a/spec/ruby/core/file/stat/birthtime_spec.rb b/spec/ruby/core/file/stat/birthtime_spec.rb index adecee15b0ec86..9aa39297b24d3c 100644 --- a/spec/ruby/core/file/stat/birthtime_spec.rb +++ b/spec/ruby/core/file/stat/birthtime_spec.rb @@ -1,7 +1,7 @@ require_relative '../../../spec_helper' platform_is(:windows, :darwin, :freebsd, :netbsd, - *ruby_version_is("3.5") { :linux }, + *ruby_version_is("4.0") { :linux }, ) do not_implemented_messages = [ "birthtime() function is unimplemented", # unsupported OS/version diff --git a/spec/ruby/core/kernel/caller_locations_spec.rb b/spec/ruby/core/kernel/caller_locations_spec.rb index 6074879d594f8f..a917dba504a931 100644 --- a/spec/ruby/core/kernel/caller_locations_spec.rb +++ b/spec/ruby/core/kernel/caller_locations_spec.rb @@ -83,7 +83,7 @@ end end - ruby_version_is "3.4"..."3.5" do + ruby_version_is "3.4"..."4.0" do it "includes core library methods defined in Ruby" do file, line = Kernel.instance_method(:tap).source_location file.should.start_with?(' { Kernel.instance_method(:tap).source_location } do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "includes core library methods defined in Ruby" do file, line = Kernel.instance_method(:tap).source_location file.should.start_with?(' *a, b { } diff --git a/spec/ruby/core/proc/source_location_spec.rb b/spec/ruby/core/proc/source_location_spec.rb index 484466f5774771..69b4e2fd8273b6 100644 --- a/spec/ruby/core/proc/source_location_spec.rb +++ b/spec/ruby/core/proc/source_location_spec.rb @@ -53,12 +53,12 @@ end it "works even if the proc was created on the same line" do - ruby_version_is(""..."3.5") do + ruby_version_is(""..."4.0") do proc { true }.source_location.should == [__FILE__, __LINE__] Proc.new { true }.source_location.should == [__FILE__, __LINE__] -> { true }.source_location.should == [__FILE__, __LINE__] end - ruby_version_is("3.5") do + ruby_version_is("4.0") do proc { true }.source_location.should == [__FILE__, __LINE__, 11, __LINE__, 19] Proc.new { true }.source_location.should == [__FILE__, __LINE__, 15, __LINE__, 23] -> { true }.source_location.should == [__FILE__, __LINE__, 8, __LINE__, 17] @@ -94,10 +94,10 @@ it "works for eval with a given line" do proc = eval('-> {}', nil, "foo", 100) location = proc.source_location - ruby_version_is(""..."3.5") do + ruby_version_is(""..."4.0") do location.should == ["foo", 100] end - ruby_version_is("3.5") do + ruby_version_is("4.0") do location.should == ["foo", 100, 2, 100, 5] end end diff --git a/spec/ruby/core/process/status/bit_and_spec.rb b/spec/ruby/core/process/status/bit_and_spec.rb index 0e0edb0afa3958..a80536462947f2 100644 --- a/spec/ruby/core/process/status/bit_and_spec.rb +++ b/spec/ruby/core/process/status/bit_and_spec.rb @@ -1,6 +1,6 @@ require_relative '../../../spec_helper' -ruby_version_is ""..."3.5" do +ruby_version_is ""..."4.0" do describe "Process::Status#&" do it "returns a bitwise and of the integer status of an exited child" do @@ -17,7 +17,7 @@ end end - ruby_version_is "3.3"..."3.5" do + ruby_version_is "3.3"..."4.0" do it "raises an ArgumentError if mask is negative" do suppress_warning do ruby_exe("exit(0)") diff --git a/spec/ruby/core/process/status/right_shift_spec.rb b/spec/ruby/core/process/status/right_shift_spec.rb index a1ab75141a3822..355aaf4c9532cb 100644 --- a/spec/ruby/core/process/status/right_shift_spec.rb +++ b/spec/ruby/core/process/status/right_shift_spec.rb @@ -1,6 +1,6 @@ require_relative '../../../spec_helper' -ruby_version_is ""..."3.5" do +ruby_version_is ""..."4.0" do describe "Process::Status#>>" do it "returns a right shift of the integer status of an exited child" do @@ -16,7 +16,7 @@ end end - ruby_version_is "3.3"..."3.5" do + ruby_version_is "3.3"..."4.0" do it "raises an ArgumentError if shift value is negative" do suppress_warning do ruby_exe("exit(0)") diff --git a/spec/ruby/core/range/max_spec.rb b/spec/ruby/core/range/max_spec.rb index 8b83f69a5a2121..09371f52987862 100644 --- a/spec/ruby/core/range/max_spec.rb +++ b/spec/ruby/core/range/max_spec.rb @@ -55,7 +55,7 @@ (..1.0).max.should == 1.0 end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "raises for an exclusive beginless Integer range" do -> { (...1).max @@ -63,7 +63,7 @@ end end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "returns the end point for exclusive beginless Integer ranges" do (...1).max.should == 0 end diff --git a/spec/ruby/core/range/reverse_each_spec.rb b/spec/ruby/core/range/reverse_each_spec.rb index b51e04c3fff24e..56390cc0da4822 100644 --- a/spec/ruby/core/range/reverse_each_spec.rb +++ b/spec/ruby/core/range/reverse_each_spec.rb @@ -88,7 +88,7 @@ (1..3).reverse_each.size.should == 3 end - ruby_bug "#20936", "3.4"..."3.5" do + ruby_bug "#20936", "3.4"..."4.0" do it "returns Infinity when Range size is infinite" do (..3).reverse_each.size.should == Float::INFINITY end diff --git a/spec/ruby/core/set/compare_by_identity_spec.rb b/spec/ruby/core/set/compare_by_identity_spec.rb index 0dda6d79f09177..238dc117a6ccfa 100644 --- a/spec/ruby/core/set/compare_by_identity_spec.rb +++ b/spec/ruby/core/set/compare_by_identity_spec.rb @@ -90,7 +90,7 @@ def o.hash; 123; end set.to_a.sort.should == [a1, a2].sort end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "raises a FrozenError on frozen sets" do set = Set.new.freeze -> { @@ -99,7 +99,7 @@ def o.hash; 123; end end end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "raises a FrozenError on frozen sets" do set = Set.new.freeze -> { diff --git a/spec/ruby/core/set/divide_spec.rb b/spec/ruby/core/set/divide_spec.rb index cbe0042f16e6ae..c6c6003e99d8b6 100644 --- a/spec/ruby/core/set/divide_spec.rb +++ b/spec/ruby/core/set/divide_spec.rb @@ -25,7 +25,7 @@ set.map{ |x| x.to_a.sort }.sort.should == [[1], [3, 4], [6], [9, 10, 11]] end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "yields each two Object to the block" do ret = [] Set[1, 2].divide { |x, y| ret << [x, y] } @@ -33,7 +33,7 @@ end end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "yields each two Object to the block" do ret = [] Set[1, 2].divide { |x, y| ret << [x, y] } diff --git a/spec/ruby/core/set/equal_value_spec.rb b/spec/ruby/core/set/equal_value_spec.rb index e3514928c816d3..721a79a3f1370b 100644 --- a/spec/ruby/core/set/equal_value_spec.rb +++ b/spec/ruby/core/set/equal_value_spec.rb @@ -24,7 +24,7 @@ set1.should == set2 end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when comparing to a Set-like object" do it "returns true when a Set and a Set-like object contain the same elements" do Set[1, 2, 3].should == SetSpecs::SetLike.new([1, 2, 3]) diff --git a/spec/ruby/core/set/flatten_merge_spec.rb b/spec/ruby/core/set/flatten_merge_spec.rb index d7c2b306579443..13cedeead953de 100644 --- a/spec/ruby/core/set/flatten_merge_spec.rb +++ b/spec/ruby/core/set/flatten_merge_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' describe "Set#flatten_merge" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "is protected" do Set.should have_protected_instance_method("flatten_merge") end diff --git a/spec/ruby/core/set/flatten_spec.rb b/spec/ruby/core/set/flatten_spec.rb index 870eccc2f10c99..f2cb3dfa524a35 100644 --- a/spec/ruby/core/set/flatten_spec.rb +++ b/spec/ruby/core/set/flatten_spec.rb @@ -16,7 +16,7 @@ -> { set.flatten }.should raise_error(ArgumentError) end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when Set contains a Set-like object" do it "returns a copy of self with each included Set-like object flattened" do Set[SetSpecs::SetLike.new([1])].flatten.should == Set[1] @@ -48,7 +48,7 @@ end version_is(set_version, ""..."1.1.0") do #ruby_version_is ""..."3.3" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when Set contains a Set-like object" do it "flattens self, including Set-like objects" do Set[SetSpecs::SetLike.new([1])].flatten!.should == Set[1] diff --git a/spec/ruby/core/set/hash_spec.rb b/spec/ruby/core/set/hash_spec.rb index 4b4696e34ccbf3..63a0aa66a55ef9 100644 --- a/spec/ruby/core/set/hash_spec.rb +++ b/spec/ruby/core/set/hash_spec.rb @@ -10,7 +10,7 @@ Set[1, 2, 3].hash.should_not == Set[:a, "b", ?c].hash end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do # see https://github.com/jruby/jruby/issues/8393 it "is equal to nil.hash for an uninitialized Set" do Set.allocate.hash.should == nil.hash diff --git a/spec/ruby/core/set/join_spec.rb b/spec/ruby/core/set/join_spec.rb index cdb593597d8641..1c1e8a8af8457d 100644 --- a/spec/ruby/core/set/join_spec.rb +++ b/spec/ruby/core/set/join_spec.rb @@ -20,7 +20,7 @@ set.join(' | ').should == "a | b | c" end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "calls #to_a to convert the Set in to an Array" do set = Set[:a, :b, :c] set.should_receive(:to_a).and_return([:a, :b, :c]) diff --git a/spec/ruby/core/set/pretty_print_cycle_spec.rb b/spec/ruby/core/set/pretty_print_cycle_spec.rb index d4cca515e2a2d3..7e6017c112b77e 100644 --- a/spec/ruby/core/set/pretty_print_cycle_spec.rb +++ b/spec/ruby/core/set/pretty_print_cycle_spec.rb @@ -3,10 +3,10 @@ describe "Set#pretty_print_cycle" do it "passes the 'pretty print' representation of a self-referencing Set to the pretty print writer" do pp = mock("PrettyPrint") - ruby_version_is(""..."3.5") do + ruby_version_is(""..."4.0") do pp.should_receive(:text).with("#") end - ruby_version_is("3.5") do + ruby_version_is("4.0") do pp.should_receive(:text).with("Set[...]") end Set[1, 2, 3].pretty_print_cycle(pp) diff --git a/spec/ruby/core/set/proper_subset_spec.rb b/spec/ruby/core/set/proper_subset_spec.rb index a84c4197c23dd6..fb7848c0015200 100644 --- a/spec/ruby/core/set/proper_subset_spec.rb +++ b/spec/ruby/core/set/proper_subset_spec.rb @@ -34,7 +34,7 @@ end version_is(set_version, ""..."1.1.0") do #ruby_version_is ""..."3.3" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when comparing to a Set-like object" do it "returns true if passed a Set-like object that self is a proper subset of" do Set[1, 2, 3].proper_subset?(SetSpecs::SetLike.new([1, 2, 3, 4])).should be_true diff --git a/spec/ruby/core/set/proper_superset_spec.rb b/spec/ruby/core/set/proper_superset_spec.rb index 653411f6b23452..dc1e87e2308e67 100644 --- a/spec/ruby/core/set/proper_superset_spec.rb +++ b/spec/ruby/core/set/proper_superset_spec.rb @@ -32,7 +32,7 @@ -> { Set[].proper_superset?(Object.new) }.should raise_error(ArgumentError) end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when comparing to a Set-like object" do it "returns true if passed a Set-like object that self is a proper superset of" do Set[1, 2, 3, 4].proper_superset?(SetSpecs::SetLike.new([1, 2, 3])).should be_true diff --git a/spec/ruby/core/set/shared/inspect.rb b/spec/ruby/core/set/shared/inspect.rb index fbc7486acd61d4..a90af66c980dbf 100644 --- a/spec/ruby/core/set/shared/inspect.rb +++ b/spec/ruby/core/set/shared/inspect.rb @@ -7,13 +7,13 @@ Set[:a, "b", Set[?c]].send(@method).should be_kind_of(String) end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "does include the elements of the set" do Set["1"].send(@method).should == 'Set["1"]' end end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "does include the elements of the set" do Set["1"].send(@method).should == '#' end @@ -23,7 +23,7 @@ Set["1", "2"].send(@method).should include('", "') end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "correctly handles cyclic-references" do set1 = Set[] set2 = Set[set1] @@ -33,7 +33,7 @@ end end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "correctly handles cyclic-references" do set1 = Set[] set2 = Set[set1] diff --git a/spec/ruby/core/set/sortedset/sortedset_spec.rb b/spec/ruby/core/set/sortedset/sortedset_spec.rb index 41f010e011c8de..f3c1ec058d80ac 100644 --- a/spec/ruby/core/set/sortedset/sortedset_spec.rb +++ b/spec/ruby/core/set/sortedset/sortedset_spec.rb @@ -1,7 +1,7 @@ require_relative '../../../spec_helper' describe "SortedSet" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "raises error including message that it has been extracted from the set stdlib" do -> { SortedSet diff --git a/spec/ruby/core/set/subset_spec.rb b/spec/ruby/core/set/subset_spec.rb index cde61d7cd7745f..112bd9b38adc12 100644 --- a/spec/ruby/core/set/subset_spec.rb +++ b/spec/ruby/core/set/subset_spec.rb @@ -34,7 +34,7 @@ end version_is(set_version, ""..."1.1.0") do #ruby_version_is ""..."3.3" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when comparing to a Set-like object" do it "returns true if passed a Set-like object that self is a subset of" do Set[1, 2, 3].subset?(SetSpecs::SetLike.new([1, 2, 3, 4])).should be_true diff --git a/spec/ruby/core/set/superset_spec.rb b/spec/ruby/core/set/superset_spec.rb index 9d7bab964a235c..9b3df2d047d4c0 100644 --- a/spec/ruby/core/set/superset_spec.rb +++ b/spec/ruby/core/set/superset_spec.rb @@ -32,7 +32,7 @@ -> { Set[].superset?(Object.new) }.should raise_error(ArgumentError) end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when comparing to a Set-like object" do it "returns true if passed a Set-like object that self is a superset of" do Set[1, 2, 3, 4].superset?(SetSpecs::SetLike.new([1, 2, 3])).should be_true diff --git a/spec/ruby/core/unboundmethod/source_location_spec.rb b/spec/ruby/core/unboundmethod/source_location_spec.rb index 2391d07d9958ef..85078ff34e8cd5 100644 --- a/spec/ruby/core/unboundmethod/source_location_spec.rb +++ b/spec/ruby/core/unboundmethod/source_location_spec.rb @@ -55,10 +55,10 @@ eval('def m; end', nil, "foo", 100) end location = c.instance_method(:m).source_location - ruby_version_is(""..."3.5") do + ruby_version_is(""..."4.0") do location.should == ["foo", 100] end - ruby_version_is("3.5") do + ruby_version_is("4.0") do location.should == ["foo", 100, 0, 100, 10] end end diff --git a/spec/ruby/language/numbered_parameters_spec.rb b/spec/ruby/language/numbered_parameters_spec.rb index 39ddd6fee83e19..de532c326d4cd1 100644 --- a/spec/ruby/language/numbered_parameters_spec.rb +++ b/spec/ruby/language/numbered_parameters_spec.rb @@ -90,14 +90,14 @@ proc { _2 }.parameters.should == [[:opt, :_1], [:opt, :_2]] end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "affects binding local variables" do -> { _1; binding.local_variables }.call("a").should == [:_1] -> { _2; binding.local_variables }.call("a", "b").should == [:_1, :_2] end end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "does not affect binding local variables" do -> { _1; binding.local_variables }.call("a").should == [] -> { _2; binding.local_variables }.call("a", "b").should == [] diff --git a/spec/ruby/language/predefined_spec.rb b/spec/ruby/language/predefined_spec.rb index d90e19858ae5e9..f2488615aaec37 100644 --- a/spec/ruby/language/predefined_spec.rb +++ b/spec/ruby/language/predefined_spec.rb @@ -687,7 +687,7 @@ def foo $VERBOSE = @verbose end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "can be assigned a String" do str = +"abc" $/ = str @@ -695,7 +695,7 @@ def foo end end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "makes a new frozen String from the assigned String" do string_subclass = Class.new(String) str = string_subclass.new("abc") @@ -763,7 +763,7 @@ def foo $VERBOSE = @verbose end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "can be assigned a String" do str = +"abc" $-0 = str @@ -771,7 +771,7 @@ def foo end end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "makes a new frozen String from the assigned String" do string_subclass = Class.new(String) str = string_subclass.new("abc") diff --git a/spec/ruby/language/regexp_spec.rb b/spec/ruby/language/regexp_spec.rb index dbf341b19ea526..ce344b5b05f067 100644 --- a/spec/ruby/language/regexp_spec.rb +++ b/spec/ruby/language/regexp_spec.rb @@ -112,7 +112,7 @@ /foo.(?<=\d)/.match("fooA foo1").to_a.should == ["foo1"] end - ruby_bug "#13671", ""..."3.5" do # https://bugs.ruby-lang.org/issues/13671 + ruby_bug "#13671", ""..."4.0" do # https://bugs.ruby-lang.org/issues/13671 it "handles a lookbehind with ss characters" do r = Regexp.new("(? "application/x-www-form-urlencoded" }.inspect.delete("{}")) diff --git a/spec/ruby/library/net-http/httpgenericrequest/exec_spec.rb b/spec/ruby/library/net-http/httpgenericrequest/exec_spec.rb index 0912e5a71f0e71..a09f9d5becec42 100644 --- a/spec/ruby/library/net-http/httpgenericrequest/exec_spec.rb +++ b/spec/ruby/library/net-http/httpgenericrequest/exec_spec.rb @@ -31,7 +31,7 @@ end describe "when a request body is set" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "sets the 'Content-Type' header to 'application/x-www-form-urlencoded' unless the 'Content-Type' header is supplied" do request = Net::HTTPGenericRequest.new("POST", true, true, "/some/path") request.body = "Some Content" @@ -64,7 +64,7 @@ end describe "when a body stream is set" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "sets the 'Content-Type' header to 'application/x-www-form-urlencoded' unless the 'Content-Type' header is supplied" do request = Net::HTTPGenericRequest.new("POST", true, true, "/some/path", "Content-Length" => "10") diff --git a/spec/ruby/library/stringscanner/check_spec.rb b/spec/ruby/library/stringscanner/check_spec.rb index 235f2f22e954fd..5e855e154ad4de 100644 --- a/spec/ruby/library/stringscanner/check_spec.rb +++ b/spec/ruby/library/stringscanner/check_spec.rb @@ -39,7 +39,6 @@ context "when #check was called with a String pattern" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil when matching succeeded" do @s.check("This") @@ -47,7 +46,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "raises IndexError when matching succeeded" do @s.check("This") @@ -68,7 +66,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) @@ -80,7 +77,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) diff --git a/spec/ruby/library/stringscanner/check_until_spec.rb b/spec/ruby/library/stringscanner/check_until_spec.rb index 701a703ebe8352..582da66b375a9f 100644 --- a/spec/ruby/library/stringscanner/check_until_spec.rb +++ b/spec/ruby/library/stringscanner/check_until_spec.rb @@ -35,7 +35,6 @@ end # https://github.com/ruby/strscan/issues/131 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.1" it "sets the last match result if given a String" do @s.check_until("a") @@ -45,7 +44,6 @@ @s.post_match.should == " test" end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "sets the last match result if given a String" do @@ -76,7 +74,6 @@ version_is StringScanner::Version, "3.1.1" do # ruby_version_is "3.4" context "when #check_until was called with a String pattern" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil when matching succeeded" do @s.check_until("This") @@ -84,7 +81,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError when matching succeeded" do @s.check_until("This") @@ -105,7 +101,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) @@ -117,7 +112,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) diff --git a/spec/ruby/library/stringscanner/exist_spec.rb b/spec/ruby/library/stringscanner/exist_spec.rb index 3f40c7a5a5a763..a408fd0b8dc1c7 100644 --- a/spec/ruby/library/stringscanner/exist_spec.rb +++ b/spec/ruby/library/stringscanner/exist_spec.rb @@ -64,7 +64,6 @@ version_is StringScanner::Version, "3.1.1" do # ruby_version_is "3.4" context "when #exist? was called with a String pattern" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil when matching succeeded" do @s.exist?("This") @@ -72,7 +71,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError when matching succeeded" do @s.exist?("This") @@ -93,7 +91,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) @@ -105,7 +102,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) diff --git a/spec/ruby/library/stringscanner/get_byte_spec.rb b/spec/ruby/library/stringscanner/get_byte_spec.rb index b3c2b7f678edd6..144859abc92a8c 100644 --- a/spec/ruby/library/stringscanner/get_byte_spec.rb +++ b/spec/ruby/library/stringscanner/get_byte_spec.rb @@ -32,7 +32,6 @@ describe "#[] successive call with a capture group name" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil" do s = StringScanner.new("This is a test") @@ -41,7 +40,6 @@ s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError" do s = StringScanner.new("This is a test") @@ -58,7 +56,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do s = StringScanner.new("This is a test") @@ -71,7 +68,6 @@ s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "ignores the previous matching with Regexp" do s = StringScanner.new("This is a test") diff --git a/spec/ruby/library/stringscanner/getch_spec.rb b/spec/ruby/library/stringscanner/getch_spec.rb index c9c3eb6fd3ecce..d369391b140ce8 100644 --- a/spec/ruby/library/stringscanner/getch_spec.rb +++ b/spec/ruby/library/stringscanner/getch_spec.rb @@ -33,7 +33,6 @@ describe "#[] successive call with a capture group name" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil" do s = StringScanner.new("This is a test") @@ -42,7 +41,6 @@ s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError" do s = StringScanner.new("This is a test") @@ -59,7 +57,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do s = StringScanner.new("This is a test") @@ -73,7 +70,6 @@ s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do s = StringScanner.new("This is a test") diff --git a/spec/ruby/library/stringscanner/scan_byte_spec.rb b/spec/ruby/library/stringscanner/scan_byte_spec.rb index c60e22be4f508c..aa2decc8f747ba 100644 --- a/spec/ruby/library/stringscanner/scan_byte_spec.rb +++ b/spec/ruby/library/stringscanner/scan_byte_spec.rb @@ -43,7 +43,6 @@ describe "#[] successive call with a capture group name" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil" do s = StringScanner.new("abc") @@ -52,7 +51,6 @@ s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError" do s = StringScanner.new("abc") @@ -69,7 +67,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do s = StringScanner.new("abc") @@ -83,7 +80,6 @@ s[:a].should == nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do s = StringScanner.new("abc") diff --git a/spec/ruby/library/stringscanner/scan_integer_spec.rb b/spec/ruby/library/stringscanner/scan_integer_spec.rb index a0b3685bae1086..fe0d26f4049076 100644 --- a/spec/ruby/library/stringscanner/scan_integer_spec.rb +++ b/spec/ruby/library/stringscanner/scan_integer_spec.rb @@ -25,7 +25,7 @@ end # https://github.com/ruby/strscan/issues/130 - ruby_bug "", "3.4"..."3.5" do # introduced in strscan v3.1.1 + ruby_bug "", "3.4"..."4.0" do # introduced in strscan v3.1.1 it "sets the last match result" do s = StringScanner.new("42abc") s.scan_integer @@ -68,7 +68,6 @@ }.should raise_error(ArgumentError, "Unsupported integer base: 5, expected 10 or 16") end - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "does not match '0x' prefix on its own" do StringScanner.new("0x").scan_integer(base: 16).should == nil @@ -76,7 +75,6 @@ StringScanner.new("+0x").scan_integer(base: 16).should == nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "matches '0' in a '0x' that is followed by non-hex characters" do @@ -96,7 +94,6 @@ describe "#[] successive call with a capture group name" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil substring when matching succeeded" do s = StringScanner.new("42") @@ -105,7 +102,6 @@ s[:a].should == nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError when matching succeeded" do s = StringScanner.new("42") @@ -131,7 +127,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "does not ignore the previous matching with Regexp" do s = StringScanner.new("42") @@ -145,7 +140,6 @@ s[:a].should == "42" end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "ignores the previous matching with Regexp" do s = StringScanner.new("42") diff --git a/spec/ruby/library/stringscanner/scan_until_spec.rb b/spec/ruby/library/stringscanner/scan_until_spec.rb index 737d83a14ca32d..610060d6f1ee25 100644 --- a/spec/ruby/library/stringscanner/scan_until_spec.rb +++ b/spec/ruby/library/stringscanner/scan_until_spec.rb @@ -41,7 +41,6 @@ end # https://github.com/ruby/strscan/issues/131 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.1" it "sets the last match result if given a String" do @s.scan_until("a") @@ -51,7 +50,6 @@ @s.post_match.should == " test" end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "sets the last match result if given a String" do @@ -82,7 +80,6 @@ version_is StringScanner::Version, "3.1.1" do # ruby_version_is "3.4" context "when #scan_until was called with a String pattern" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil when matching succeeded" do @s.scan_until("This") @@ -90,7 +87,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError when matching succeeded" do @s.scan_until("This") @@ -111,7 +107,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) @@ -123,7 +118,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) diff --git a/spec/ruby/library/stringscanner/search_full_spec.rb b/spec/ruby/library/stringscanner/search_full_spec.rb index a089da2043b1ea..197adfda4d4519 100644 --- a/spec/ruby/library/stringscanner/search_full_spec.rb +++ b/spec/ruby/library/stringscanner/search_full_spec.rb @@ -50,7 +50,6 @@ end # https://github.com/ruby/strscan/issues/131 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.1" it "sets the last match result if given a String" do @s.search_full("is a", false, false) @@ -60,7 +59,6 @@ @s.post_match.should == " test" end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "sets the last match result if given a String" do @@ -91,7 +89,6 @@ version_is StringScanner::Version, "3.1.1" do # ruby_version_is "3.4" context "when #search_full was called with a String pattern" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil when matching succeeded" do @s.search_full("This", false, false) @@ -99,7 +96,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError when matching succeeded" do @s.search_full("This", false, false) diff --git a/spec/ruby/library/stringscanner/skip_until_spec.rb b/spec/ruby/library/stringscanner/skip_until_spec.rb index f5be4b5ceb0a15..5d73d8f0b91104 100644 --- a/spec/ruby/library/stringscanner/skip_until_spec.rb +++ b/spec/ruby/library/stringscanner/skip_until_spec.rb @@ -38,7 +38,6 @@ end # https://github.com/ruby/strscan/issues/131 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.1" it "sets the last match result if given a String" do @s.skip_until("a") @@ -48,7 +47,6 @@ @s.post_match.should == " test" end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "sets the last match result if given a String" do @@ -79,7 +77,6 @@ version_is StringScanner::Version, "3.1.1" do # ruby_version_is "3.4" context "when #skip_until was called with a String pattern" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil when matching succeeded" do @s.skip_until("This") @@ -87,7 +84,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError when matching succeeded" do @s.skip_until("This") @@ -108,7 +104,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) @@ -120,7 +115,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) diff --git a/spec/ruby/optional/capi/ext/rubyspec.h b/spec/ruby/optional/capi/ext/rubyspec.h index 8aaec36f465818..6c4bea5da0e124 100644 --- a/spec/ruby/optional/capi/ext/rubyspec.h +++ b/spec/ruby/optional/capi/ext/rubyspec.h @@ -35,8 +35,8 @@ (RUBY_API_VERSION_MAJOR == (major) && RUBY_API_VERSION_MINOR < (minor))) #define RUBY_VERSION_SINCE(major,minor) (!RUBY_VERSION_BEFORE(major, minor)) -#if RUBY_VERSION_SINCE(3, 5) -#define RUBY_VERSION_IS_3_5 +#if RUBY_VERSION_SINCE(4, 0) +#define RUBY_VERSION_IS_4_0 #endif #if RUBY_VERSION_SINCE(3, 4) diff --git a/spec/ruby/optional/capi/ext/set_spec.c b/spec/ruby/optional/capi/ext/set_spec.c index 7af922fd49ea96..11a271b361ba6b 100644 --- a/spec/ruby/optional/capi/ext/set_spec.c +++ b/spec/ruby/optional/capi/ext/set_spec.c @@ -1,7 +1,7 @@ #include "ruby.h" #include "rubyspec.h" -#ifdef RUBY_VERSION_IS_3_5 +#ifdef RUBY_VERSION_IS_4_0 #ifdef __cplusplus extern "C" { #endif diff --git a/spec/ruby/optional/capi/ext/thread_spec.c b/spec/ruby/optional/capi/ext/thread_spec.c index 6ee111b7b7ea72..ac77e4e813b517 100644 --- a/spec/ruby/optional/capi/ext/thread_spec.c +++ b/spec/ruby/optional/capi/ext/thread_spec.c @@ -166,7 +166,7 @@ static VALUE thread_spec_ruby_native_thread_p_new_thread(VALUE self) { #endif } -#ifdef RUBY_VERSION_IS_3_5 +#ifdef RUBY_VERSION_IS_4_0 static VALUE thread_spec_ruby_thread_has_gvl_p(VALUE self) { return ruby_thread_has_gvl_p() ? Qtrue : Qfalse; } @@ -185,7 +185,7 @@ void Init_thread_spec(void) { rb_define_method(cls, "rb_thread_create", thread_spec_rb_thread_create, 2); rb_define_method(cls, "ruby_native_thread_p", thread_spec_ruby_native_thread_p, 0); rb_define_method(cls, "ruby_native_thread_p_new_thread", thread_spec_ruby_native_thread_p_new_thread, 0); -#ifdef RUBY_VERSION_IS_3_5 +#ifdef RUBY_VERSION_IS_4_0 rb_define_method(cls, "ruby_thread_has_gvl_p", thread_spec_ruby_thread_has_gvl_p, 0); #endif } diff --git a/spec/ruby/optional/capi/set_spec.rb b/spec/ruby/optional/capi/set_spec.rb index 3b7ee812c56ade..3e35be0505fffa 100644 --- a/spec/ruby/optional/capi/set_spec.rb +++ b/spec/ruby/optional/capi/set_spec.rb @@ -1,6 +1,6 @@ require_relative 'spec_helper' -ruby_version_is "3.5" do +ruby_version_is "4.0" do load_extension("set") describe "C-API Set function" do diff --git a/spec/ruby/optional/capi/string_spec.rb b/spec/ruby/optional/capi/string_spec.rb index 605c43769ddb0b..72f20ee6a52455 100644 --- a/spec/ruby/optional/capi/string_spec.rb +++ b/spec/ruby/optional/capi/string_spec.rb @@ -193,7 +193,7 @@ def inspect it "returns a new String object filled with \\0 bytes" do lens = [4] - ruby_version_is "3.5" do + ruby_version_is "4.0" do lens << 100 end @@ -1230,7 +1230,7 @@ def inspect -> { str.upcase! }.should raise_error(RuntimeError, 'can\'t modify string; temporarily locked') end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "raises FrozenError if string is frozen" do str = -"rb_str_locktmp" -> { @s.rb_str_locktmp(str) }.should raise_error(FrozenError) @@ -1254,7 +1254,7 @@ def inspect -> { @s.rb_str_unlocktmp(+"test") }.should raise_error(RuntimeError, 'temporal unlocking already unlocked string') end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "raises FrozenError if string is frozen" do str = -"rb_str_locktmp" -> { @s.rb_str_unlocktmp(str) }.should raise_error(FrozenError) diff --git a/spec/ruby/optional/capi/thread_spec.rb b/spec/ruby/optional/capi/thread_spec.rb index cd9ae8ff1923bb..117726f0e2a392 100644 --- a/spec/ruby/optional/capi/thread_spec.rb +++ b/spec/ruby/optional/capi/thread_spec.rb @@ -185,7 +185,7 @@ def call_capi_rb_thread_wakeup end end - ruby_version_is "3.5" do + ruby_version_is "4.0" do describe "ruby_thread_has_gvl_p" do it "returns true if the current thread has the GVL" do @t.ruby_thread_has_gvl_p.should be_true diff --git a/spec/ruby/security/cve_2020_10663_spec.rb b/spec/ruby/security/cve_2020_10663_spec.rb index 80e860348b10ad..c44a13a0dd4b5d 100644 --- a/spec/ruby/security/cve_2020_10663_spec.rb +++ b/spec/ruby/security/cve_2020_10663_spec.rb @@ -1,6 +1,6 @@ require_relative '../spec_helper' -ruby_version_is ""..."3.5" do +ruby_version_is ""..."4.0" do require 'json' module JSONSpecs diff --git a/spec/ruby/shared/kernel/raise.rb b/spec/ruby/shared/kernel/raise.rb index 8432c835946d6e..2be06ea797aa6d 100644 --- a/spec/ruby/shared/kernel/raise.rb +++ b/spec/ruby/shared/kernel/raise.rb @@ -141,7 +141,7 @@ def e.exception end end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "allows cause keyword argument" do cause = StandardError.new("original error") result = nil @@ -272,7 +272,7 @@ def e.exception end describe :kernel_raise_across_contexts, shared: true do - ruby_version_is "3.5" do + ruby_version_is "4.0" do describe "with cause keyword argument" do it "uses the cause from the calling context" do original_cause = nil diff --git a/test/error_highlight/test_error_highlight.rb b/test/error_highlight/test_error_highlight.rb index 1276a0a0d93a7b..d3ca99021b9ad2 100644 --- a/test/error_highlight/test_error_highlight.rb +++ b/test/error_highlight/test_error_highlight.rb @@ -1733,6 +1733,62 @@ def test_spot_with_node assert_equal expected_spot, actual_spot end + module SingletonMethodWithSpacing + LINENO = __LINE__ + 1 + def self . baz(x:) + x + end + end + + def test_singleton_method_with_spacing_missing_keyword + lineno = __LINE__ + assert_error_message(ArgumentError, <<~END) do +missing keyword: :x (ArgumentError) + + caller: #{ __FILE__ }:#{ lineno + 16 } + | SingletonMethodWithSpacing.baz + ^^^^ + callee: #{ __FILE__ }:#{ SingletonMethodWithSpacing::LINENO } + #{ + MethodDefLocationSupported ? + "| def self . baz(x:) + ^^^^^" : + "(cannot highlight method definition; try Ruby 4.0 or later)" + } + END + + SingletonMethodWithSpacing.baz + end + end + + module SingletonMethodMultipleKwargs + LINENO = __LINE__ + 1 + def self.run(shop_id:, param1:) + shop_id + param1 + end + end + + def test_singleton_method_multiple_missing_keywords + lineno = __LINE__ + assert_error_message(ArgumentError, <<~END) do +missing keywords: :shop_id, :param1 (ArgumentError) + + caller: #{ __FILE__ }:#{ lineno + 16 } + | SingletonMethodMultipleKwargs.run + ^^^^ + callee: #{ __FILE__ }:#{ SingletonMethodMultipleKwargs::LINENO } + #{ + MethodDefLocationSupported ? + "| def self.run(shop_id:, param1:) + ^^^^" : + "(cannot highlight method definition; try Ruby 4.0 or later)" + } + END + + SingletonMethodMultipleKwargs.run + end + end + private def find_node_by_id(node, node_id) diff --git a/test/ruby/test_gc.rb b/test/ruby/test_gc.rb index 7695fd33cf9945..6639013a54ca32 100644 --- a/test/ruby/test_gc.rb +++ b/test/ruby/test_gc.rb @@ -231,6 +231,9 @@ def test_stat_heap end assert_equal (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]) * (2**i), stat_heap[:slot_size] + assert_operator stat_heap[:heap_live_slots], :<=, stat[:heap_live_slots] + assert_operator stat_heap[:heap_free_slots], :<=, stat[:heap_free_slots] + assert_operator stat_heap[:heap_final_slots], :<=, stat[:heap_final_slots] assert_operator stat_heap[:heap_eden_pages], :<=, stat[:heap_eden_pages] assert_operator stat_heap[:heap_eden_slots], :>=, 0 assert_operator stat_heap[:total_allocated_pages], :>=, 0 @@ -261,7 +264,7 @@ def test_stat_heap_all GC.stat_heap(i, stat_heap) # Remove keys that can vary between invocations - %i(total_allocated_objects).each do |sym| + %i(total_allocated_objects heap_live_slots heap_free_slots).each do |sym| stat_heap[sym] = stat_heap_all[i][sym] = 0 end @@ -286,6 +289,9 @@ def test_stat_heap_constraints hash.each { |k, v| stat_heap_sum[k] += v } end + assert_equal stat[:heap_live_slots], stat_heap_sum[:heap_live_slots] + assert_equal stat[:heap_free_slots], stat_heap_sum[:heap_free_slots] + assert_equal stat[:heap_final_slots], stat_heap_sum[:heap_final_slots] assert_equal stat[:heap_eden_pages], stat_heap_sum[:heap_eden_pages] assert_equal stat[:heap_available_slots], stat_heap_sum[:heap_eden_slots] assert_equal stat[:total_allocated_objects], stat_heap_sum[:total_allocated_objects] diff --git a/tool/test/test_sync_default_gems.rb b/tool/test/test_sync_default_gems.rb index cdbbb0c5394dc3..4a2688850be12a 100755 --- a/tool/test/test_sync_default_gems.rb +++ b/tool/test/test_sync_default_gems.rb @@ -319,6 +319,9 @@ def test_delete_after_conflict end def test_squash_merge + if RUBY_PLATFORM =~ /s390x/ + omit("git 2.43.0 bug on s390x ubuntu 24.04: BUG: log-tree.c:1058: did a remerge diff without remerge_objdir?!?") + end # 2---. <- branch # / \ # 1---3---3'<- merge commit with conflict resolution diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 8495ee59ef438e..7626d461352c8d 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -6041,11 +6041,14 @@ vm_define_method(const rb_execution_context_t *ec, VALUE obj, ID id, VALUE iseqv } // Return the untagged block handler: +// * If it's VM_BLOCK_HANDLER_NONE, return nil // * If it's an ISEQ or an IFUNC, fetch it from its rb_captured_block // * If it's a PROC or SYMBOL, return it as is static VALUE rb_vm_untag_block_handler(VALUE block_handler) { + if (VM_BLOCK_HANDLER_NONE == block_handler) return Qnil; + switch (vm_block_handler_type(block_handler)) { case block_handler_type_iseq: case block_handler_type_ifunc: { diff --git a/zjit.rb b/zjit.rb index bb6d4d3cdca16c..fc306c19a47fba 100644 --- a/zjit.rb +++ b/zjit.rb @@ -174,6 +174,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) # 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/build.rs b/zjit/build.rs index 6aec5407f62af7..4ee3d65b33062e 100644 --- a/zjit/build.rs +++ b/zjit/build.rs @@ -5,9 +5,11 @@ fn main() { // option_env! automatically registers a rerun-if-env-changed if let Some(ruby_build_dir) = option_env!("RUBY_BUILD_DIR") { - // Link against libminiruby + // Link against libminiruby.a println!("cargo:rustc-link-search=native={ruby_build_dir}"); println!("cargo:rustc-link-lib=static:-bundle=miniruby"); + // Re-link when libminiruby.a changes + println!("cargo:rerun-if-changed={ruby_build_dir}/libminiruby.a"); // System libraries that libminiruby needs. Has to be // ordered after -lminiruby above. diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index cb8382a43c940c..3c9bf72023d90b 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -2065,6 +2065,17 @@ impl Assembler { out } + pub fn count_call_to(&mut self, fn_name: &str) { + // We emit ccalls while initializing the JIT. Unfortunately, we skip those because + // otherwise we have no counter pointers to read. + if crate::state::ZJITState::has_instance() && get_option!(stats) { + let ccall_counter_pointers = crate::state::ZJITState::get_ccall_counter_pointers(); + let counter_ptr = ccall_counter_pointers.entry(fn_name.to_string()).or_insert_with(|| Box::new(0)); + let counter_ptr: &mut u64 = counter_ptr.as_mut(); + self.incr_counter(Opnd::const_ptr(counter_ptr), 1.into()); + } + } + pub fn cmp(&mut self, left: Opnd, right: Opnd) { self.push_insn(Insn::Cmp { left, right }); } @@ -2389,6 +2400,7 @@ pub(crate) use asm_comment; macro_rules! asm_ccall { [$asm: ident, $fn_name:ident, $($args:expr),* ] => {{ $crate::backend::lir::asm_comment!($asm, concat!("call ", stringify!($fn_name))); + $asm.count_call_to(stringify!($fn_name)); $asm.ccall($fn_name as *const u8, vec![$($args),*]) }}; } diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 8fc66791a665ad..18266b46933e6c 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -419,14 +419,14 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::GuardLess { left, right, state } => gen_guard_less(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)), &Insn::GuardGreaterEq { left, right, state } => gen_guard_greater_eq(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)), Insn::PatchPoint { invariant, state } => no_output!(gen_patch_point(jit, asm, invariant, &function.frame_state(*state))), - Insn::CCall { cfunc, args, name: _, return_type: _, elidable: _ } => gen_ccall(asm, *cfunc, opnds!(args)), + Insn::CCall { cfunc, args, name, return_type: _, elidable: _ } => gen_ccall(asm, *cfunc, *name, opnds!(args)), // Give up CCallWithFrame for 7+ args since asm.ccall() doesn't support it. Insn::CCallWithFrame { cd, state, args, .. } if args.len() > C_ARG_OPNDS.len() => gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), SendFallbackReason::CCallWithFrameTooManyArgs), - Insn::CCallWithFrame { cfunc, args, cme, state, blockiseq, .. } => - gen_ccall_with_frame(jit, asm, *cfunc, opnds!(args), *cme, *blockiseq, &function.frame_state(*state)), - Insn::CCallVariadic { cfunc, recv, args, name: _, cme, state, return_type: _, elidable: _ } => { - gen_ccall_variadic(jit, asm, *cfunc, opnd!(recv), opnds!(args), *cme, &function.frame_state(*state)) + Insn::CCallWithFrame { cfunc, name, args, cme, state, blockiseq, .. } => + gen_ccall_with_frame(jit, asm, *cfunc, *name, opnds!(args), *cme, *blockiseq, &function.frame_state(*state)), + Insn::CCallVariadic { cfunc, recv, args, name, cme, state, return_type: _, elidable: _ } => { + gen_ccall_variadic(jit, asm, *cfunc, *name, opnd!(recv), opnds!(args), *cme, &function.frame_state(*state)) } Insn::GetIvar { self_val, id, state: _ } => gen_getivar(asm, opnd!(self_val), *id), Insn::SetGlobal { id, val, state } => no_output!(gen_setglobal(jit, asm, *id, opnd!(val), &function.frame_state(*state))), @@ -697,6 +697,7 @@ fn gen_invokebuiltin(jit: &JITState, asm: &mut Assembler, state: &FrameState, bf let mut cargs = vec![EC]; cargs.extend(args); + asm.count_call_to(unsafe { std::ffi::CStr::from_ptr(bf.name).to_str().unwrap() }); asm.ccall(bf.func_ptr as *const u8, cargs) } @@ -754,6 +755,7 @@ fn gen_ccall_with_frame( jit: &mut JITState, asm: &mut Assembler, cfunc: *const u8, + name: ID, args: Vec, cme: *const rb_callable_method_entry_t, blockiseq: Option, @@ -801,6 +803,7 @@ fn gen_ccall_with_frame( asm.mov(CFP, new_cfp); asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); + asm.count_call_to(&name.contents_lossy()); let result = asm.ccall(cfunc, args); asm_comment!(asm, "pop C frame"); @@ -817,7 +820,8 @@ fn gen_ccall_with_frame( /// Lowering for [`Insn::CCall`]. This is a low-level raw call that doesn't know /// anything about the callee, so handling for e.g. GC safety is dealt with elsewhere. -fn gen_ccall(asm: &mut Assembler, cfunc: *const u8, args: Vec) -> lir::Opnd { +fn gen_ccall(asm: &mut Assembler, cfunc: *const u8, name: ID, args: Vec) -> lir::Opnd { + asm.count_call_to(&name.contents_lossy()); asm.ccall(cfunc, args) } @@ -827,6 +831,7 @@ fn gen_ccall_variadic( jit: &mut JITState, asm: &mut Assembler, cfunc: *const u8, + name: ID, recv: Opnd, args: Vec, cme: *const rb_callable_method_entry_t, @@ -859,6 +864,7 @@ fn gen_ccall_variadic( asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); let argv_ptr = gen_push_opnds(asm, &args); + asm.count_call_to(&name.contents_lossy()); let result = asm.ccall(cfunc, vec![args.len().into(), argv_ptr, recv]); gen_pop_opnds(asm, &args); @@ -1169,9 +1175,10 @@ fn gen_send( unsafe extern "C" { fn rb_vm_send(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE; } - asm.ccall( - rb_vm_send as *const u8, - vec![EC, CFP, Opnd::const_ptr(cd), VALUE::from(blockiseq).into()], + asm_ccall!( + asm, + rb_vm_send, + EC, CFP, Opnd::const_ptr(cd), VALUE::from(blockiseq).into() ) } @@ -1192,9 +1199,10 @@ fn gen_send_forward( unsafe extern "C" { fn rb_vm_sendforward(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE; } - asm.ccall( - rb_vm_sendforward as *const u8, - vec![EC, CFP, Opnd::const_ptr(cd), VALUE::from(blockiseq).into()], + asm_ccall!( + asm, + rb_vm_sendforward, + EC, CFP, Opnd::const_ptr(cd), VALUE::from(blockiseq).into() ) } @@ -1213,9 +1221,10 @@ fn gen_send_without_block( unsafe extern "C" { fn rb_vm_opt_send_without_block(ec: EcPtr, cfp: CfpPtr, cd: VALUE) -> VALUE; } - asm.ccall( - rb_vm_opt_send_without_block as *const u8, - vec![EC, CFP, Opnd::const_ptr(cd)], + asm_ccall!( + asm, + rb_vm_opt_send_without_block, + EC, CFP, Opnd::const_ptr(cd) ) } @@ -1331,9 +1340,10 @@ fn gen_invokeblock( unsafe extern "C" { fn rb_vm_invokeblock(ec: EcPtr, cfp: CfpPtr, cd: VALUE) -> VALUE; } - asm.ccall( - rb_vm_invokeblock as *const u8, - vec![EC, CFP, Opnd::const_ptr(cd)], + asm_ccall!( + asm, + rb_vm_invokeblock, + EC, CFP, Opnd::const_ptr(cd) ) } @@ -1353,9 +1363,10 @@ fn gen_invokesuper( unsafe extern "C" { fn rb_vm_invokesuper(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE; } - asm.ccall( - rb_vm_invokesuper as *const u8, - vec![EC, CFP, Opnd::const_ptr(cd), VALUE::from(blockiseq).into()], + asm_ccall!( + asm, + rb_vm_invokesuper, + EC, CFP, Opnd::const_ptr(cd), VALUE::from(blockiseq).into() ) } @@ -1436,9 +1447,10 @@ fn gen_array_include( unsafe extern "C" { fn rb_vm_opt_newarray_include_p(ec: EcPtr, num: c_long, elts: *const VALUE, target: VALUE) -> VALUE; } - asm.ccall( - rb_vm_opt_newarray_include_p as *const u8, - vec![EC, num.into(), elements_ptr, target], + asm_ccall!( + asm, + rb_vm_opt_newarray_include_p, + EC, num.into(), elements_ptr, target ) } @@ -1454,9 +1466,10 @@ fn gen_dup_array_include( unsafe extern "C" { fn rb_vm_opt_duparray_include_p(ec: EcPtr, ary: VALUE, target: VALUE) -> VALUE; } - asm.ccall( - rb_vm_opt_duparray_include_p as *const u8, - vec![EC, ary.into(), target], + asm_ccall!( + asm, + rb_vm_opt_duparray_include_p, + EC, ary.into(), target ) } @@ -1527,6 +1540,7 @@ fn gen_object_alloc_class(asm: &mut Assembler, class: VALUE, state: &FrameState) let alloc_func = unsafe { rb_zjit_class_get_alloc_func(class) }; assert!(alloc_func.is_some(), "class {} passed to ObjectAllocClass must have an allocator", get_class_name(class)); asm_comment!(asm, "call allocator for class {}", get_class_name(class)); + asm.count_call_to(&format!("{}::allocator", get_class_name(class))); asm.ccall(alloc_func.unwrap() as *const u8, vec![class.into()]) } } diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index db47385bc88321..61c25a4092bdc4 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -775,11 +775,17 @@ pub fn rust_str_to_ruby(str: &str) -> VALUE { unsafe { rb_utf8_str_new(str.as_ptr() as *const _, str.len() as i64) } } -/// Produce a Ruby symbol from a Rust string slice -pub fn rust_str_to_sym(str: &str) -> VALUE { +/// Produce a Ruby ID from a Rust string slice +pub fn rust_str_to_id(str: &str) -> ID { let c_str = CString::new(str).unwrap(); let c_ptr: *const c_char = c_str.as_ptr(); - unsafe { rb_id2sym(rb_intern(c_ptr)) } + unsafe { rb_intern(c_ptr) } +} + +/// Produce a Ruby symbol from a Rust string slice +pub fn rust_str_to_sym(str: &str) -> VALUE { + let id = rust_str_to_id(str); + unsafe { rb_id2sym(id) } } /// Produce an owned Rust String from a C char pointer diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 58638f30f0264d..2640507e33fab5 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -6,7 +6,7 @@ #![allow(clippy::if_same_then_else)] #![allow(clippy::match_like_matches_macro)] use crate::{ - cast::IntoUsize, codegen::local_idx_to_ep_offset, cruby::*, payload::{get_or_create_iseq_payload, IseqPayload}, options::{debug, get_option, DumpHIR}, state::ZJITState + cast::IntoUsize, codegen::local_idx_to_ep_offset, cruby::*, payload::{get_or_create_iseq_payload, IseqPayload}, options::{debug, get_option, DumpHIR}, state::ZJITState, json::Json }; use std::{ cell::RefCell, collections::{HashMap, HashSet, VecDeque}, ffi::{c_void, c_uint, c_int, CStr}, fmt::Display, mem::{align_of, size_of}, ptr, slice::Iter @@ -39,7 +39,7 @@ impl std::fmt::Display for InsnId { } /// The index of a [`Block`], which effectively acts like a pointer. -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)] pub struct BlockId(pub usize); impl From for usize { @@ -1485,8 +1485,7 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq return false; } - // Check argument count against callee's parameters. Note that correctness for this calculation - // relies on rejecting features above. + // Because we exclude e.g. post parameters above, they are also excluded from the sum below. let lead_num = unsafe { get_iseq_body_param_lead_num(iseq) }; let opt_num = unsafe { get_iseq_body_param_opt_num(iseq) }; can_send = c_int::try_from(args.len()) @@ -2889,12 +2888,13 @@ impl Function { let mut cfunc_args = vec![recv]; cfunc_args.append(&mut args); + let name = rust_str_to_id(&qualified_method_name(unsafe { (*cme).owner }, unsafe { (*cme).called_id })); let ccall = fun.push_insn(block, Insn::CCallWithFrame { cd, cfunc, args: cfunc_args, cme, - name: method_id, + name, state, return_type: types::BasicObject, elidable: false, @@ -3018,6 +3018,7 @@ impl Function { // No inlining; emit a call let cfunc = unsafe { get_mct_func(cfunc) }.cast(); + let name = rust_str_to_id(&qualified_method_name(unsafe { (*cme).owner }, unsafe { (*cme).called_id })); let mut cfunc_args = vec![recv]; cfunc_args.append(&mut args); let return_type = props.return_type; @@ -3025,7 +3026,7 @@ impl Function { // Filter for a leaf and GC free function if props.leaf && props.no_gc { fun.push_insn(block, Insn::IncrCounter(Counter::inline_cfunc_optimized_send_count)); - let ccall = fun.push_insn(block, Insn::CCall { cfunc, args: cfunc_args, name: method_id, return_type, elidable }); + let ccall = fun.push_insn(block, Insn::CCall { cfunc, args: cfunc_args, name, return_type, elidable }); fun.make_equal_to(send_insn_id, ccall); } else { if get_option!(stats) { @@ -3036,7 +3037,7 @@ impl Function { cfunc, args: cfunc_args, cme, - name: method_id, + name, state, return_type, elidable, @@ -3099,12 +3100,13 @@ impl Function { } let return_type = props.return_type; let elidable = props.elidable; + let name = rust_str_to_id(&qualified_method_name(unsafe { (*cme).owner }, unsafe { (*cme).called_id })); let ccall = fun.push_insn(block, Insn::CCallVariadic { cfunc, recv, args, cme, - name: method_id, + name, state, return_type, elidable, @@ -3684,23 +3686,171 @@ impl Function { } } + /// Helper function to make an Iongraph JSON "instruction". + /// `uses`, `memInputs` and `attributes` are left empty for now, but may be populated + /// in the future. + fn make_iongraph_instr(id: InsnId, inputs: Vec, opcode: &str, ty: &str) -> Json { + Json::object() + // Add an offset of 0x1000 to avoid the `ptr` being 0x0, which iongraph rejects. + .insert("ptr", id.0 + 0x1000) + .insert("id", id.0) + .insert("opcode", opcode) + .insert("attributes", Json::empty_array()) + .insert("inputs", Json::Array(inputs)) + .insert("uses", Json::empty_array()) + .insert("memInputs", Json::empty_array()) + .insert("type", ty) + .build() + } + + /// Helper function to make an Iongraph JSON "block". + fn make_iongraph_block(id: BlockId, predecessors: Vec, successors: Vec, instructions: Vec, attributes: Vec<&str>, loop_depth: u32) -> Json { + Json::object() + // Add an offset of 0x1000 to avoid the `ptr` being 0x0, which iongraph rejects. + .insert("ptr", id.0 + 0x1000) + .insert("id", id.0) + .insert("loopDepth", loop_depth) + .insert("attributes", Json::array(attributes)) + .insert("predecessors", Json::array(predecessors.iter().map(|x| x.0).collect::>())) + .insert("successors", Json::array(successors.iter().map(|x| x.0).collect::>())) + .insert("instructions", Json::array(instructions)) + .build() + } + + /// Helper function to make an Iongraph JSON "function". + /// Note that `lir` is unpopulated right now as ZJIT doesn't use its functionality. + fn make_iongraph_function(pass_name: &str, hir_blocks: Vec) -> Json { + Json::object() + .insert("name", pass_name) + .insert("mir", Json::object() + .insert("blocks", Json::array(hir_blocks)) + .build() + ) + .insert("lir", Json::object() + .insert("blocks", Json::empty_array()) + .build() + ) + .build() + } + + /// Generate an iongraph JSON pass representation for this function. + pub fn to_iongraph_pass(&self, pass_name: &str) -> Json { + let mut ptr_map = PtrPrintMap::identity(); + if cfg!(test) { + ptr_map.map_ptrs = true; + } + + let mut hir_blocks = Vec::new(); + let cfi = ControlFlowInfo::new(self); + let dominators = Dominators::new(self); + let loop_info = LoopInfo::new(&cfi, &dominators); + + // Push each block from the iteration in reverse post order to `hir_blocks`. + for block_id in self.rpo() { + // Create the block with instructions. + let block = &self.blocks[block_id.0]; + let predecessors = cfi.predecessors(block_id).collect(); + let successors = cfi.successors(block_id).collect(); + let mut instructions = Vec::new(); + + // Process all instructions (parameters and body instructions). + // Parameters are currently guaranteed to be Parameter instructions, but in the future + // they might be refined to other instruction kinds by the optimizer. + for insn_id in block.params.iter().chain(block.insns.iter()) { + let insn_id = self.union_find.borrow().find_const(*insn_id); + let insn = self.find(insn_id); + + // Snapshots are not serialized, so skip them. + if matches!(insn, Insn::Snapshot {..}) { + continue; + } + + // Instructions with no output or an empty type should have an empty type field. + let type_str = if insn.has_output() { + let insn_type = self.type_of(insn_id); + if insn_type.is_subtype(types::Empty) { + String::new() + } else { + insn_type.print(&ptr_map).to_string() + } + } else { + String::new() + }; + + + let opcode = insn.print(&ptr_map).to_string(); + + // Traverse the worklist to get inputs for a given instruction. + let mut inputs = VecDeque::new(); + self.worklist_traverse_single_insn(&insn, &mut inputs); + let inputs: Vec = inputs.into_iter().map(|x| x.0.into()).collect(); + + instructions.push( + Self::make_iongraph_instr( + insn_id, + inputs, + &opcode, + &type_str + ) + ); + } + + let mut attributes = vec![]; + if loop_info.is_back_edge_source(block_id) { + attributes.push("backedge"); + } + if loop_info.is_loop_header(block_id) { + attributes.push("loopheader"); + } + let loop_depth = loop_info.loop_depth(block_id); + + hir_blocks.push(Self::make_iongraph_block( + block_id, + predecessors, + successors, + instructions, + attributes, + loop_depth, + )); + } + + Self::make_iongraph_function(pass_name, hir_blocks) + } + /// Run all the optimization passes we have. pub fn optimize(&mut self) { + let mut passes: Vec = Vec::new(); + let should_dump = get_option!(dump_hir_iongraph); + + macro_rules! run_pass { + ($name:ident) => { + self.$name(); + #[cfg(debug_assertions)] self.assert_validates(); + if should_dump { + passes.push( + self.to_iongraph_pass(stringify!($name)) + ); + } + } + } + + if should_dump { + passes.push(self.to_iongraph_pass("unoptimized")); + } + // Function is assumed to have types inferred already - self.type_specialize(); - #[cfg(debug_assertions)] self.assert_validates(); - self.inline(); - #[cfg(debug_assertions)] self.assert_validates(); - self.optimize_getivar(); - #[cfg(debug_assertions)] self.assert_validates(); - self.optimize_c_calls(); - #[cfg(debug_assertions)] self.assert_validates(); - self.fold_constants(); - #[cfg(debug_assertions)] self.assert_validates(); - self.clean_cfg(); - #[cfg(debug_assertions)] self.assert_validates(); - self.eliminate_dead_code(); - #[cfg(debug_assertions)] self.assert_validates(); + run_pass!(type_specialize); + run_pass!(inline); + run_pass!(optimize_getivar); + run_pass!(optimize_c_calls); + run_pass!(fold_constants); + run_pass!(clean_cfg); + run_pass!(eliminate_dead_code); + + if should_dump { + let iseq_name = iseq_get_location(self.iseq, 0); + self.dump_iongraph(&iseq_name, passes); + } } /// Dump HIR passed to codegen if specified by options. @@ -3721,6 +3871,32 @@ impl Function { } } + pub fn dump_iongraph(&self, function_name: &str, passes: Vec) { + fn sanitize_for_filename(name: &str) -> String { + name.chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '_' || c == '-' { + c + } else { + '_' + } + }) + .collect() + } + + use std::io::Write; + let dir = format!("/tmp/zjit-iongraph-{}", std::process::id()); + std::fs::create_dir_all(&dir).expect("Unable to create directory."); + let sanitized = sanitize_for_filename(function_name); + let path = format!("{dir}/func_{sanitized}.json"); + let mut file = std::fs::File::create(path).unwrap(); + let json = Json::object() + .insert("name", function_name) + .insert("passes", passes) + .build(); + writeln!(file, "{}", json).unwrap(); + } + /// Validates the following: /// 1. Basic block jump args match parameter arity. /// 2. Every terminator must be in the last position. @@ -4085,7 +4261,13 @@ impl Function { impl<'a> std::fmt::Display for FunctionPrinter<'a> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let fun = &self.fun; - let iseq_name = iseq_get_location(fun.iseq, 0); + // In tests, there may not be an iseq to get location from. + let iseq_name = if fun.iseq.is_null() { + String::from("") + } else { + iseq_get_location(fun.iseq, 0) + }; + // In tests, strip the line number for builtin ISEQs to make tests stable across line changes let iseq_name = if cfg!(test) && iseq_name.contains("@ Result { let jit_entry_insns = jit_entry_insns(iseq); let BytecodeInfo { jump_targets, has_blockiseq } = compute_bytecode_info(iseq, &jit_entry_insns); - // Make all empty basic blocks. The ordering of the BBs matters as it is taken as a schedule - // in the backend without a scheduling pass. TODO: Higher quality scheduling during lowering. + // Make all empty basic blocks. The ordering of the BBs matters for getting fallthrough jumps + // in good places, but it's not necessary for correctness. TODO: Higher quality scheduling during lowering. let mut insn_idx_to_block = HashMap::new(); // Make blocks for optionals first, and put them right next to their JIT entrypoint for insn_idx in jit_entry_insns.iter().copied() { @@ -5557,6 +5739,258 @@ fn compile_jit_entry_state(fun: &mut Function, jit_entry_block: BlockId, jit_ent (self_param, entry_state) } +pub struct Dominators<'a> { + f: &'a Function, + dominators: Vec>, +} + +impl<'a> Dominators<'a> { + pub fn new(f: &'a Function) -> Self { + let mut cfi = ControlFlowInfo::new(f); + Self::with_cfi(f, &mut cfi) + } + + pub fn with_cfi(f: &'a Function, cfi: &mut ControlFlowInfo) -> Self { + let block_ids = f.rpo(); + let mut dominators = vec![vec![]; f.blocks.len()]; + + // Compute dominators for each node using fixed point iteration. + // Approach can be found in Figure 1 of: + // https://www.cs.tufts.edu/~nr/cs257/archive/keith-cooper/dom14.pdf + // + // Initially we set: + // + // dom(entry) = {entry} for each entry block + // dom(b != entry) = {all nodes} + // + // Iteratively, apply: + // + // dom(b) = {b} union intersect(dom(p) for p in predecessors(b)) + // + // When we've run the algorithm and the dominator set no longer changes + // between iterations, then we have found the dominator sets. + + // Set up entry blocks. + // Entry blocks are only dominated by themselves. + for entry_block in &f.entry_blocks() { + dominators[entry_block.0] = vec![*entry_block]; + } + + // Setup the initial dominator sets. + for block_id in &block_ids { + if !f.entry_blocks().contains(block_id) { + // Non entry blocks are initially dominated by all other blocks. + dominators[block_id.0] = block_ids.clone(); + } + } + + let mut changed = true; + while changed { + changed = false; + + for block_id in &block_ids { + if *block_id == f.entry_block { + continue; + } + + // Get all predecessors for a given block. + let block_preds: Vec = cfi.predecessors(*block_id).collect(); + if block_preds.is_empty() { + continue; + } + + let mut new_doms = dominators[block_preds[0].0].clone(); + + // Compute the intersection of predecessor dominator sets into `new_doms`. + for pred_id in &block_preds[1..] { + let pred_doms = &dominators[pred_id.0]; + // Only keep a dominator in `new_doms` if it is also found in pred_doms + new_doms.retain(|d| pred_doms.contains(d)); + } + + // Insert sorted into `new_doms`. + match new_doms.binary_search(block_id) { + Ok(_) => {} + Err(pos) => new_doms.insert(pos, *block_id) + } + + // If we have computed a new dominator set, then we can update + // the dominators and mark that we need another iteration. + if dominators[block_id.0] != new_doms { + dominators[block_id.0] = new_doms; + changed = true; + } + } + } + + Self { f, dominators } + } + + + pub fn is_dominated_by(&self, left: BlockId, right: BlockId) -> bool { + self.dominators(left).any(|&b| b == right) + } + + pub fn dominators(&self, block: BlockId) -> Iter<'_, BlockId> { + self.dominators[block.0].iter() + } +} + +pub struct ControlFlowInfo<'a> { + function: &'a Function, + successor_map: HashMap>, + predecessor_map: HashMap>, +} + +impl<'a> ControlFlowInfo<'a> { + pub fn new(function: &'a Function) -> Self { + let mut successor_map: HashMap> = HashMap::new(); + let mut predecessor_map: HashMap> = HashMap::new(); + let uf = function.union_find.borrow(); + + for block_id in function.rpo() { + let block = &function.blocks[block_id.0]; + + // Since ZJIT uses extended basic blocks, one must check all instructions + // for their ability to jump to another basic block, rather than just + // the instructions at the end of a given basic block. + let successors: Vec = block + .insns + .iter() + .map(|&insn_id| uf.find_const(insn_id)) + .filter_map(|insn_id| { + Self::extract_jump_target(&function.insns[insn_id.0]) + }) + .collect(); + + // Update predecessors for successor blocks. + for &succ_id in &successors { + predecessor_map + .entry(succ_id) + .or_default() + .push(block_id); + } + + // Store successors for this block. + successor_map.insert(block_id, successors); + } + + Self { + function, + successor_map, + predecessor_map, + } + } + + pub fn is_succeeded_by(&self, left: BlockId, right: BlockId) -> bool { + self.successor_map.get(&right).is_some_and(|set| set.contains(&left)) + } + + pub fn is_preceded_by(&self, left: BlockId, right: BlockId) -> bool { + self.predecessor_map.get(&right).is_some_and(|set| set.contains(&left)) + } + + pub fn predecessors(&self, block: BlockId) -> impl Iterator { + self.predecessor_map.get(&block).into_iter().flatten().copied() + } + + pub fn successors(&self, block: BlockId) -> impl Iterator { + self.successor_map.get(&block).into_iter().flatten().copied() + } + + /// Helper function to extract the target of a jump instruction. + fn extract_jump_target(insn: &Insn) -> Option { + match insn { + Insn::Jump(target) + | Insn::IfTrue { target, .. } + | Insn::IfFalse { target, .. } => Some(target.target), + _ => None, + } + } +} + +pub struct LoopInfo<'a> { + cfi: &'a ControlFlowInfo<'a>, + dominators: &'a Dominators<'a>, + loop_depths: HashMap, + loop_headers: BlockSet, + back_edge_sources: BlockSet, +} + +impl<'a> LoopInfo<'a> { + pub fn new(cfi: &'a ControlFlowInfo<'a>, dominators: &'a Dominators<'a>) -> Self { + let mut loop_headers: BlockSet = BlockSet::with_capacity(cfi.function.num_blocks()); + let mut loop_depths: HashMap = HashMap::new(); + let mut back_edge_sources: BlockSet = BlockSet::with_capacity(cfi.function.num_blocks()); + let rpo = cfi.function.rpo(); + + for &block in &rpo { + loop_depths.insert(block, 0); + } + + // Collect loop headers. + for &block in &rpo { + // Initialize the loop depths. + for predecessor in cfi.predecessors(block) { + if dominators.is_dominated_by(predecessor, block) { + // Found a loop header, so then identify the natural loop. + loop_headers.insert(block); + back_edge_sources.insert(predecessor); + let loop_blocks = Self::find_natural_loop(cfi, block, predecessor); + // Increment the loop depth. + for loop_block in &loop_blocks { + *loop_depths.get_mut(loop_block).expect("Loop block should be populated.") += 1; + } + } + } + } + + Self { + cfi, + dominators, + loop_depths, + loop_headers, + back_edge_sources, + } + } + + fn find_natural_loop( + cfi: &ControlFlowInfo, + header: BlockId, + back_edge_source: BlockId, + ) -> HashSet { + // todo(aidenfoxivey): Reimplement using BlockSet + let mut loop_blocks = HashSet::new(); + let mut stack = vec![back_edge_source]; + + loop_blocks.insert(header); + loop_blocks.insert(back_edge_source); + + while let Some(block) = stack.pop() { + for pred in cfi.predecessors(block) { + // Pushes to stack only if `pred` wasn't already in `loop_blocks`. + if loop_blocks.insert(pred) { + stack.push(pred) + } + } + } + + loop_blocks + } + + pub fn loop_depth(&self, block: BlockId) -> u32 { + self.loop_depths.get(&block).copied().unwrap_or(0) + } + + pub fn is_back_edge_source(&self, block: BlockId) -> bool { + self.back_edge_sources.get(block) + } + + pub fn is_loop_header(&self, block: BlockId) -> bool { + self.loop_headers.get(block) + } +} + #[cfg(test)] mod union_find_tests { use super::UnionFind; diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index fadb6ced5f1c0a..19f0e91b47e82b 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -560,7 +560,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(CustomEq@0x1000, !=@0x1008, cme:0x1010) PatchPoint NoSingletonClass(CustomEq@0x1000) v28:HeapObject[class_exact:CustomEq] = GuardType v9, HeapObject[class_exact:CustomEq] - v29:BoolExact = CCallWithFrame !=@0x1038, v28, v9 + v29:BoolExact = CCallWithFrame BasicObject#!=@0x1038, v28, v9 v20:NilClass = Const Value(nil) CheckInterrupts Return v20 @@ -784,7 +784,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(C@0x1000, fun_new_map@0x1008, cme:0x1010) PatchPoint NoSingletonClass(C@0x1000) v24:ArraySubclass[class_exact:C] = GuardType v13, ArraySubclass[class_exact:C] - v25:BasicObject = CCallWithFrame fun_new_map@0x1038, v24, block=0x1040 + v25:BasicObject = CCallWithFrame C#fun_new_map@0x1038, v24, block=0x1040 v16:BasicObject = GetLocal l0, EP@3 CheckInterrupts Return v25 @@ -1043,7 +1043,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Object@0x1008, puts@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Object@0x1008) v22:HeapObject[class_exact*:Object@VALUE(0x1008)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1008)] - v23:BasicObject = CCallVariadic puts@0x1040, v22, v12 + v23:BasicObject = CCallVariadic Kernel#puts@0x1040, v22, v12 CheckInterrupts Return v23 "); @@ -2241,7 +2241,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Module@0x1010, name@0x1018, cme:0x1020) PatchPoint NoSingletonClass(Module@0x1010) IncrCounter inline_cfunc_optimized_send_count - v34:StringExact|NilClass = CCall name@0x1048, v29 + v34:StringExact|NilClass = CCall Module#name@0x1048, v29 PatchPoint NoEPEscape(test) v22:Fixnum[1] = Const Value(1) CheckInterrupts @@ -2273,7 +2273,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, length@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) IncrCounter inline_cfunc_optimized_send_count - v29:Fixnum = CCall length@0x1038, v13 + v29:Fixnum = CCall Array#length@0x1038, v13 v20:Fixnum[5] = Const Value(5) CheckInterrupts Return v20 @@ -2417,7 +2417,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, size@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) IncrCounter inline_cfunc_optimized_send_count - v29:Fixnum = CCall size@0x1038, v13 + v29:Fixnum = CCall Array#size@0x1038, v13 v20:Fixnum[5] = Const Value(5) CheckInterrupts Return v20 @@ -3150,7 +3150,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1008, new@0x1010, cme:0x1018) PatchPoint MethodRedefined(Class@0x1040, new@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Class@0x1040) - v57:BasicObject = CCallVariadic new@0x1048, v46, v16 + v57:BasicObject = CCallVariadic Array.new@0x1048, v46, v16 CheckInterrupts Return v57 "); @@ -3181,7 +3181,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Set@0x1008, initialize@0x1040, cme:0x1048) PatchPoint NoSingletonClass(Set@0x1008) v49:SetExact = GuardType v18, SetExact - v50:BasicObject = CCallVariadic initialize@0x1070, v49 + v50:BasicObject = CCallVariadic Set#initialize@0x1070, v49 CheckInterrupts CheckInterrupts Return v18 @@ -3211,7 +3211,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(String@0x1008, new@0x1010, cme:0x1018) PatchPoint MethodRedefined(Class@0x1040, new@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Class@0x1040) - v54:BasicObject = CCallVariadic new@0x1048, v43 + v54:BasicObject = CCallVariadic String.new@0x1048, v43 CheckInterrupts Return v54 "); @@ -3243,7 +3243,7 @@ mod hir_opt_tests { v50:RegexpExact = ObjectAllocClass Regexp:VALUE(0x1008) PatchPoint MethodRedefined(Regexp@0x1008, initialize@0x1048, cme:0x1050) PatchPoint NoSingletonClass(Regexp@0x1008) - v54:BasicObject = CCallVariadic initialize@0x1078, v50, v17 + v54:BasicObject = CCallVariadic Regexp#initialize@0x1078, v50, v17 CheckInterrupts CheckInterrupts Return v50 @@ -3271,7 +3271,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, length@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) IncrCounter inline_cfunc_optimized_send_count - v30:Fixnum = CCall length@0x1038, v18 + v30:Fixnum = CCall Array#length@0x1038, v18 CheckInterrupts Return v30 "); @@ -3298,7 +3298,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, size@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) IncrCounter inline_cfunc_optimized_send_count - v30:Fixnum = CCall size@0x1038, v18 + v30:Fixnum = CCall Array#size@0x1038, v18 CheckInterrupts Return v30 "); @@ -3458,7 +3458,7 @@ mod hir_opt_tests { v10:HashExact = NewHash PatchPoint MethodRedefined(Hash@0x1000, dup@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Hash@0x1000) - v22:BasicObject = CCallWithFrame dup@0x1038, v10 + v22:BasicObject = CCallWithFrame Kernel#dup@0x1038, v10 v14:BasicObject = SendWithoutBlock v22, :freeze CheckInterrupts Return v14 @@ -3551,7 +3551,7 @@ mod hir_opt_tests { v10:ArrayExact = NewArray PatchPoint MethodRedefined(Array@0x1000, dup@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) - v22:BasicObject = CCallWithFrame dup@0x1038, v10 + v22:BasicObject = CCallWithFrame Kernel#dup@0x1038, v10 v14:BasicObject = SendWithoutBlock v22, :freeze CheckInterrupts Return v14 @@ -3645,7 +3645,7 @@ mod hir_opt_tests { v11:StringExact = StringCopy v10 PatchPoint MethodRedefined(String@0x1008, dup@0x1010, cme:0x1018) PatchPoint NoSingletonClass(String@0x1008) - v23:BasicObject = CCallWithFrame dup@0x1040, v11 + v23:BasicObject = CCallWithFrame String#dup@0x1040, v11 v15:BasicObject = SendWithoutBlock v23, :freeze CheckInterrupts Return v15 @@ -3740,7 +3740,7 @@ mod hir_opt_tests { v11:StringExact = StringCopy v10 PatchPoint MethodRedefined(String@0x1008, dup@0x1010, cme:0x1018) PatchPoint NoSingletonClass(String@0x1008) - v23:BasicObject = CCallWithFrame dup@0x1040, v11 + v23:BasicObject = CCallWithFrame String#dup@0x1040, v11 v15:BasicObject = SendWithoutBlock v23, :-@ CheckInterrupts Return v15 @@ -3882,7 +3882,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1008, to_s@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Array@0x1008) v31:ArrayExact = GuardType v9, ArrayExact - v32:BasicObject = CCallWithFrame to_s@0x1040, v31 + v32:BasicObject = CCallWithFrame Array#to_s@0x1040, v31 v19:String = AnyToString v9, str: v32 v21:StringExact = StringConcat v13, v19 CheckInterrupts @@ -4745,7 +4745,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Array@0x1000) v23:ArrayExact = GuardType v9, ArrayExact IncrCounter inline_cfunc_optimized_send_count - v25:BoolExact = CCall empty?@0x1038, v23 + v25:BoolExact = CCall Array#empty?@0x1038, v23 CheckInterrupts Return v25 "); @@ -4773,7 +4773,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Hash@0x1000) v23:HashExact = GuardType v9, HashExact IncrCounter inline_cfunc_optimized_send_count - v25:BoolExact = CCall empty?@0x1038, v23 + v25:BoolExact = CCall Hash#empty?@0x1038, v23 CheckInterrupts Return v25 "); @@ -5036,7 +5036,7 @@ mod hir_opt_tests { v11:ArrayExact = ArrayDup v10 PatchPoint MethodRedefined(Array@0x1008, map@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Array@0x1008) - v21:BasicObject = CCallWithFrame map@0x1040, v11, block=0x1048 + v21:BasicObject = CCallWithFrame Array#map@0x1040, v11, block=0x1048 CheckInterrupts Return v21 "); @@ -5484,7 +5484,7 @@ mod hir_opt_tests { v10:ArrayExact = NewArray PatchPoint MethodRedefined(Array@0x1000, reverse@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) - v20:ArrayExact = CCallWithFrame reverse@0x1038, v10 + v20:ArrayExact = CCallWithFrame Array#reverse@0x1038, v10 CheckInterrupts Return v20 "); @@ -5537,7 +5537,7 @@ mod hir_opt_tests { v13:StringExact = StringCopy v12 PatchPoint MethodRedefined(Array@0x1008, join@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Array@0x1008) - v23:StringExact = CCallVariadic join@0x1040, v10, v13 + v23:StringExact = CCallVariadic Array#join@0x1040, v10, v13 CheckInterrupts Return v23 "); @@ -5859,7 +5859,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Class@0x1010, current@0x1018, cme:0x1020) PatchPoint NoSingletonClass(Class@0x1010) IncrCounter inline_cfunc_optimized_send_count - v25:BasicObject = CCall current@0x1048, v20 + v25:BasicObject = CCall Thread.current@0x1048, v20 CheckInterrupts Return v25 "); @@ -5889,7 +5889,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, []=@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) v31:ArrayExact = GuardType v9, ArrayExact - v32:BasicObject = CCallVariadic []=@0x1038, v31, v16, v18 + v32:BasicObject = CCallVariadic Array#[]=@0x1038, v31, v16, v18 CheckInterrupts Return v18 "); @@ -5980,7 +5980,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, push@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) v28:ArrayExact = GuardType v9, ArrayExact - v29:BasicObject = CCallVariadic push@0x1038, v28, v14, v16, v18 + v29:BasicObject = CCallVariadic Array#push@0x1038, v28, v14, v16, v18 CheckInterrupts Return v29 "); @@ -6008,7 +6008,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Array@0x1000) v23:ArrayExact = GuardType v9, ArrayExact IncrCounter inline_cfunc_optimized_send_count - v25:Fixnum = CCall length@0x1038, v23 + v25:Fixnum = CCall Array#length@0x1038, v23 CheckInterrupts Return v25 "); @@ -6036,7 +6036,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Array@0x1000) v23:ArrayExact = GuardType v9, ArrayExact IncrCounter inline_cfunc_optimized_send_count - v25:Fixnum = CCall size@0x1038, v23 + v25:Fixnum = CCall Array#size@0x1038, v23 CheckInterrupts Return v25 "); @@ -6064,7 +6064,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(String@0x1008, =~@0x1010, cme:0x1018) PatchPoint NoSingletonClass(String@0x1008) v25:StringExact = GuardType v9, StringExact - v26:BasicObject = CCallWithFrame =~@0x1040, v25, v14 + v26:BasicObject = CCallWithFrame String#=~@0x1040, v25, v14 CheckInterrupts Return v26 "); @@ -6235,7 +6235,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(String@0x1000, setbyte@0x1008, cme:0x1010) PatchPoint NoSingletonClass(String@0x1000) v30:StringExact = GuardType v13, StringExact - v31:BasicObject = CCallWithFrame setbyte@0x1038, v30, v14, v15 + v31:BasicObject = CCallWithFrame String#setbyte@0x1038, v30, v14, v15 CheckInterrupts Return v31 "); @@ -6264,7 +6264,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(String@0x1000) v23:StringExact = GuardType v9, StringExact IncrCounter inline_cfunc_optimized_send_count - v25:BoolExact = CCall empty?@0x1038, v23 + v25:BoolExact = CCall String#empty?@0x1038, v23 CheckInterrupts Return v25 "); @@ -6348,7 +6348,7 @@ mod hir_opt_tests { bb2(v8:BasicObject, v9:BasicObject): PatchPoint MethodRedefined(Integer@0x1000, succ@0x1008, cme:0x1010) v22:Integer = GuardType v9, Integer - v23:BasicObject = CCallWithFrame succ@0x1038, v22 + v23:BasicObject = CCallWithFrame Integer#succ@0x1038, v22 CheckInterrupts Return v23 "); @@ -6405,7 +6405,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(String@0x1000, <<@0x1008, cme:0x1010) PatchPoint NoSingletonClass(String@0x1000) v27:StringExact = GuardType v11, StringExact - v28:BasicObject = CCallWithFrame <<@0x1038, v27, v12 + v28:BasicObject = CCallWithFrame String#<<@0x1038, v27, v12 CheckInterrupts Return v28 "); @@ -6465,7 +6465,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(MyString@0x1000, <<@0x1008, cme:0x1010) PatchPoint NoSingletonClass(MyString@0x1000) v27:StringSubclass[class_exact:MyString] = GuardType v11, StringSubclass[class_exact:MyString] - v28:BasicObject = CCallWithFrame <<@0x1038, v27, v12 + v28:BasicObject = CCallWithFrame String#<<@0x1038, v27, v12 CheckInterrupts Return v28 "); @@ -6622,7 +6622,7 @@ mod hir_opt_tests { bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): PatchPoint MethodRedefined(Integer@0x1000, ^@0x1008, cme:0x1010) v25:Integer = GuardType v11, Integer - v26:BasicObject = CCallWithFrame ^@0x1038, v25, v12 + v26:BasicObject = CCallWithFrame Integer#^@0x1038, v25, v12 CheckInterrupts Return v26 "); @@ -6645,7 +6645,7 @@ mod hir_opt_tests { bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): PatchPoint MethodRedefined(Integer@0x1000, ^@0x1008, cme:0x1010) v25:Fixnum = GuardType v11, Fixnum - v26:BasicObject = CCallWithFrame ^@0x1038, v25, v12 + v26:BasicObject = CCallWithFrame Integer#^@0x1038, v25, v12 CheckInterrupts Return v26 "); @@ -6668,7 +6668,7 @@ mod hir_opt_tests { bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): PatchPoint MethodRedefined(TrueClass@0x1000, ^@0x1008, cme:0x1010) v25:TrueClass = GuardType v11, TrueClass - v26:BasicObject = CCallWithFrame ^@0x1038, v25, v12 + v26:BasicObject = CCallWithFrame TrueClass#^@0x1038, v25, v12 CheckInterrupts Return v26 "); @@ -6718,7 +6718,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Hash@0x1000) v23:HashExact = GuardType v9, HashExact IncrCounter inline_cfunc_optimized_send_count - v25:Fixnum = CCall size@0x1038, v23 + v25:Fixnum = CCall Hash#size@0x1038, v23 CheckInterrupts Return v25 "); @@ -7086,7 +7086,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(C@0x1008, respond_to?@0x1010, cme:0x1018) PatchPoint NoSingletonClass(C@0x1008) v24:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] - v25:BasicObject = CCallVariadic respond_to?@0x1040, v24, v14 + v25:BasicObject = CCallVariadic Kernel#respond_to?@0x1040, v24, v14 CheckInterrupts Return v25 "); @@ -7637,7 +7637,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(String@0x1000) v23:StringExact = GuardType v9, StringExact IncrCounter inline_cfunc_optimized_send_count - v25:Fixnum = CCall size@0x1038, v23 + v25:Fixnum = CCall String#size@0x1038, v23 CheckInterrupts Return v25 "); @@ -7756,7 +7756,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(String@0x1000) v23:StringExact = GuardType v9, StringExact IncrCounter inline_cfunc_optimized_send_count - v25:Fixnum = CCall length@0x1038, v23 + v25:Fixnum = CCall String#length@0x1038, v23 CheckInterrupts Return v25 "); @@ -7922,7 +7922,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Class@0x1038) v30:ModuleSubclass[class_exact*:Class@VALUE(0x1038)] = GuardType v26, ModuleSubclass[class_exact*:Class@VALUE(0x1038)] IncrCounter inline_cfunc_optimized_send_count - v32:StringExact|NilClass = CCall name@0x1070, v30 + v32:StringExact|NilClass = CCall Module#name@0x1070, v30 CheckInterrupts Return v32 "); diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index abf2f9497c2875..a00ca97e85a8b0 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -3425,3 +3425,821 @@ pub mod hir_build_tests { "); } } + + /// Test successor and predecessor set computations. + #[cfg(test)] + mod control_flow_info_tests { + use super::*; + + fn edge(target: BlockId) -> BranchEdge { + BranchEdge { target, args: vec![] } + } + + #[test] + fn test_linked_list() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb1))); + function.push_insn(bb1, Insn::Jump(edge(bb2))); + function.push_insn(bb2, Insn::Jump(edge(bb3))); + + let retval = function.push_insn(bb3, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb3, Insn::Return { val: retval }); + + let cfi = ControlFlowInfo::new(&function); + + assert!(cfi.is_preceded_by(bb1, bb2)); + assert!(cfi.is_succeeded_by(bb2, bb1)); + assert!(cfi.predecessors(bb3).eq([bb2])); + } + + #[test] + fn test_diamond() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + + let v1 = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb0, Insn::IfTrue { val: v1, target: edge(bb2)}); + function.push_insn(bb0, Insn::Jump(edge(bb1))); + function.push_insn(bb1, Insn::Jump(edge(bb3))); + function.push_insn(bb2, Insn::Jump(edge(bb3))); + + let retval = function.push_insn(bb3, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb3, Insn::Return { val: retval }); + + let cfi = ControlFlowInfo::new(&function); + + assert!(cfi.is_preceded_by(bb2, bb3)); + assert!(cfi.is_preceded_by(bb1, bb3)); + assert!(!cfi.is_preceded_by(bb0, bb3)); + assert!(cfi.is_succeeded_by(bb1, bb0)); + assert!(cfi.is_succeeded_by(bb3, bb1)); + } + } + + /// Test dominator set computations. + #[cfg(test)] + mod dom_tests { + use super::*; + use insta::assert_snapshot; + + fn edge(target: BlockId) -> BranchEdge { + BranchEdge { target, args: vec![] } + } + + fn assert_dominators_contains_self(function: &Function, dominators: &Dominators) { + for (i, _) in function.blocks.iter().enumerate() { + // Ensure that each dominating set contains the block itself. + assert!(dominators.is_dominated_by(BlockId(i), BlockId(i))); + } + } + + #[test] + fn test_linked_list() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb1))); + function.push_insn(bb1, Insn::Jump(edge(bb2))); + function.push_insn(bb2, Insn::Jump(edge(bb3))); + + let retval = function.push_insn(bb3, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb3, Insn::Return { val: retval }); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + Jump bb1() + bb1(): + Jump bb2() + bb2(): + Jump bb3() + bb3(): + v3:Any = Const CBool(true) + Return v3 + "); + + let dominators = Dominators::new(&function); + assert_dominators_contains_self(&function, &dominators); + assert!(dominators.dominators(bb0).eq([bb0].iter())); + assert!(dominators.dominators(bb1).eq([bb0, bb1].iter())); + assert!(dominators.dominators(bb2).eq([bb0, bb1, bb2].iter())); + assert!(dominators.dominators(bb3).eq([bb0, bb1, bb2, bb3].iter())); + } + + #[test] + fn test_diamond() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + + let val = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb0, Insn::IfTrue { val, target: edge(bb1)}); + function.push_insn(bb0, Insn::Jump(edge(bb2))); + + function.push_insn(bb2, Insn::Jump(edge(bb3))); + function.push_insn(bb1, Insn::Jump(edge(bb3))); + + let retval = function.push_insn(bb3, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb3, Insn::Return { val: retval }); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + v0:Any = Const Value(false) + IfTrue v0, bb1() + Jump bb2() + bb1(): + Jump bb3() + bb2(): + Jump bb3() + bb3(): + v5:Any = Const CBool(true) + Return v5 + "); + + let dominators = Dominators::new(&function); + assert_dominators_contains_self(&function, &dominators); + assert!(dominators.dominators(bb0).eq([bb0].iter())); + assert!(dominators.dominators(bb1).eq([bb0, bb1].iter())); + assert!(dominators.dominators(bb2).eq([bb0, bb2].iter())); + assert!(dominators.dominators(bb3).eq([bb0, bb3].iter())); + } + + #[test] + fn test_complex_cfg() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + let bb4 = function.new_block(0); + let bb5 = function.new_block(0); + let bb6 = function.new_block(0); + let bb7 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb1))); + + let v0 = function.push_insn(bb1, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb1, Insn::IfTrue { val: v0, target: edge(bb2)}); + function.push_insn(bb1, Insn::Jump(edge(bb4))); + + function.push_insn(bb2, Insn::Jump(edge(bb3))); + + let v1 = function.push_insn(bb3, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb3, Insn::IfTrue { val: v1, target: edge(bb5)}); + function.push_insn(bb3, Insn::Jump(edge(bb7))); + + function.push_insn(bb4, Insn::Jump(edge(bb5))); + + function.push_insn(bb5, Insn::Jump(edge(bb6))); + + function.push_insn(bb6, Insn::Jump(edge(bb7))); + + let retval = function.push_insn(bb7, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb7, Insn::Return { val: retval }); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + Jump bb1() + bb1(): + v1:Any = Const Value(false) + IfTrue v1, bb2() + Jump bb4() + bb2(): + Jump bb3() + bb3(): + v5:Any = Const Value(false) + IfTrue v5, bb5() + Jump bb7() + bb4(): + Jump bb5() + bb5(): + Jump bb6() + bb6(): + Jump bb7() + bb7(): + v11:Any = Const CBool(true) + Return v11 + "); + + let dominators = Dominators::new(&function); + assert_dominators_contains_self(&function, &dominators); + assert!(dominators.dominators(bb0).eq([bb0].iter())); + assert!(dominators.dominators(bb1).eq([bb0, bb1].iter())); + assert!(dominators.dominators(bb2).eq([bb0, bb1, bb2].iter())); + assert!(dominators.dominators(bb3).eq([bb0, bb1, bb2, bb3].iter())); + assert!(dominators.dominators(bb4).eq([bb0, bb1, bb4].iter())); + assert!(dominators.dominators(bb5).eq([bb0, bb1, bb5].iter())); + assert!(dominators.dominators(bb6).eq([bb0, bb1, bb5, bb6].iter())); + assert!(dominators.dominators(bb7).eq([bb0, bb1, bb7].iter())); + } + + #[test] + fn test_back_edges() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + let bb4 = function.new_block(0); + let bb5 = function.new_block(0); + + let v0 = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb0, Insn::IfTrue { val: v0, target: edge(bb1)}); + function.push_insn(bb0, Insn::Jump(edge(bb4))); + + let v1 = function.push_insn(bb1, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb1, Insn::IfTrue { val: v1, target: edge(bb2)}); + function.push_insn(bb1, Insn::Jump(edge(bb3))); + + function.push_insn(bb2, Insn::Jump(edge(bb3))); + + function.push_insn(bb4, Insn::Jump(edge(bb5))); + + let v2 = function.push_insn(bb5, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb5, Insn::IfTrue { val: v2, target: edge(bb3)}); + function.push_insn(bb5, Insn::Jump(edge(bb4))); + + let retval = function.push_insn(bb3, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb3, Insn::Return { val: retval }); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + v0:Any = Const Value(false) + IfTrue v0, bb1() + Jump bb4() + bb1(): + v3:Any = Const Value(false) + IfTrue v3, bb2() + Jump bb3() + bb2(): + Jump bb3() + bb4(): + Jump bb5() + bb5(): + v8:Any = Const Value(false) + IfTrue v8, bb3() + Jump bb4() + bb3(): + v11:Any = Const CBool(true) + Return v11 + "); + + let dominators = Dominators::new(&function); + assert_dominators_contains_self(&function, &dominators); + assert!(dominators.dominators(bb0).eq([bb0].iter())); + assert!(dominators.dominators(bb1).eq([bb0, bb1].iter())); + assert!(dominators.dominators(bb2).eq([bb0, bb1, bb2].iter())); + assert!(dominators.dominators(bb3).eq([bb0, bb3].iter())); + assert!(dominators.dominators(bb4).eq([bb0, bb4].iter())); + assert!(dominators.dominators(bb5).eq([bb0, bb4, bb5].iter())); + } + + #[test] + fn test_multiple_entry_blocks() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + function.jit_entry_blocks.push(bb1); + let bb2 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb2))); + + function.push_insn(bb1, Insn::Jump(edge(bb2))); + + let retval = function.push_insn(bb2, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb2, Insn::Return { val: retval }); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + Jump bb2() + bb1(): + Jump bb2() + bb2(): + v2:Any = Const CBool(true) + Return v2 + "); + + let dominators = Dominators::new(&function); + assert_dominators_contains_self(&function, &dominators); + + assert!(dominators.dominators(bb1).eq([bb1].iter())); + assert!(dominators.dominators(bb2).eq([bb2].iter())); + + assert!(!dominators.is_dominated_by(bb1, bb2)); + } + } + + /// Test loop information computation. +#[cfg(test)] +mod loop_info_tests { + use super::*; + use insta::assert_snapshot; + + fn edge(target: BlockId) -> BranchEdge { + BranchEdge { target, args: vec![] } + } + + #[test] + fn test_loop_depth() { + // ┌─────┐ + // │ bb0 │ + // └──┬──┘ + // │ + // ┌──▼──┐ ┌─────┐ + // │ bb2 ◄──────┼ bb1 ◄─┐ + // └──┬──┘ └─────┘ │ + // └─────────────────┘ + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb2))); + + let val = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb2, Insn::IfTrue { val, target: edge(bb1)}); + let retval = function.push_insn(bb2, Insn::Const { val: Const::CBool(true) }); + let _ = function.push_insn(bb2, Insn::Return { val: retval }); + + function.push_insn(bb1, Insn::Jump(edge(bb2))); + + let cfi = ControlFlowInfo::new(&function); + let dominators = Dominators::new(&function); + let loop_info = LoopInfo::new(&cfi, &dominators); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + Jump bb2() + v1:Any = Const Value(false) + bb2(): + IfTrue v1, bb1() + v3:Any = Const CBool(true) + Return v3 + bb1(): + Jump bb2() + "); + + assert!(loop_info.is_loop_header(bb2)); + assert!(loop_info.is_back_edge_source(bb1)); + assert_eq!(loop_info.loop_depth(bb1), 1); + } + + #[test] + fn test_nested_loops() { + // ┌─────┐ + // │ bb0 ◄─────┐ + // └──┬──┘ │ + // │ │ + // ┌──▼──┐ │ + // │ bb1 ◄───┐ │ + // └──┬──┘ │ │ + // │ │ │ + // ┌──▼──┐ │ │ + // │ bb2 ┼───┘ │ + // └──┬──┘ │ + // │ │ + // ┌──▼──┐ │ + // │ bb3 ┼─────┘ + // └──┬──┘ + // │ + // ┌──▼──┐ + // │ bb4 │ + // └─────┘ + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + let bb4 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb1))); + + function.push_insn(bb1, Insn::Jump(edge(bb2))); + + let cond = function.push_insn(bb2, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb2, Insn::IfTrue { val: cond, target: edge(bb1) }); + function.push_insn(bb2, Insn::Jump(edge(bb3))); + + let cond = function.push_insn(bb3, Insn::Const { val: Const::Value(Qtrue) }); + let _ = function.push_insn(bb3, Insn::IfTrue { val: cond, target: edge(bb0) }); + function.push_insn(bb3, Insn::Jump(edge(bb4))); + + let retval = function.push_insn(bb4, Insn::Const { val: Const::CBool(true) }); + let _ = function.push_insn(bb4, Insn::Return { val: retval }); + + let cfi = ControlFlowInfo::new(&function); + let dominators = Dominators::new(&function); + let loop_info = LoopInfo::new(&cfi, &dominators); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + Jump bb1() + bb1(): + Jump bb2() + bb2(): + v2:Any = Const Value(false) + IfTrue v2, bb1() + Jump bb3() + bb3(): + v5:Any = Const Value(true) + IfTrue v5, bb0() + Jump bb4() + bb4(): + v8:Any = Const CBool(true) + Return v8 + "); + + assert!(loop_info.is_loop_header(bb0)); + assert!(loop_info.is_loop_header(bb1)); + + assert_eq!(loop_info.loop_depth(bb0), 1); + assert_eq!(loop_info.loop_depth(bb1), 2); + assert_eq!(loop_info.loop_depth(bb2), 2); + assert_eq!(loop_info.loop_depth(bb3), 1); + assert_eq!(loop_info.loop_depth(bb4), 0); + + assert!(loop_info.is_back_edge_source(bb2)); + assert!(loop_info.is_back_edge_source(bb3)); + } + + #[test] + fn test_complex_loops() { + // ┌─────┐ + // ┌──────► bb0 │ + // │ └──┬──┘ + // │ ┌────┴────┐ + // │ ┌──▼──┐ ┌──▼──┐ + // │ │ bb1 ◄─┐ │ bb3 ◄─┐ + // │ └──┬──┘ │ └──┬──┘ │ + // │ │ │ │ │ + // │ ┌──▼──┐ │ ┌──▼──┐ │ + // │ │ bb2 ┼─┘ │ bb4 ┼─┘ + // │ └──┬──┘ └──┬──┘ + // │ └────┬────┘ + // │ ┌──▼──┐ + // └──────┼ bb5 │ + // └──┬──┘ + // │ + // ┌──▼──┐ + // │ bb6 │ + // └─────┘ + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + let bb4 = function.new_block(0); + let bb5 = function.new_block(0); + let bb6 = function.new_block(0); + + let cond = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb0, Insn::IfTrue { val: cond, target: edge(bb1) }); + function.push_insn(bb0, Insn::Jump(edge(bb3))); + + function.push_insn(bb1, Insn::Jump(edge(bb2))); + + let _ = function.push_insn(bb2, Insn::IfTrue { val: cond, target: edge(bb1) }); + function.push_insn(bb2, Insn::Jump(edge(bb5))); + + function.push_insn(bb3, Insn::Jump(edge(bb4))); + + let _ = function.push_insn(bb4, Insn::IfTrue { val: cond, target: edge(bb3) }); + function.push_insn(bb4, Insn::Jump(edge(bb5))); + + let _ = function.push_insn(bb5, Insn::IfTrue { val: cond, target: edge(bb0) }); + function.push_insn(bb5, Insn::Jump(edge(bb6))); + + let retval = function.push_insn(bb6, Insn::Const { val: Const::CBool(true) }); + let _ = function.push_insn(bb6, Insn::Return { val: retval }); + + let cfi = ControlFlowInfo::new(&function); + let dominators = Dominators::new(&function); + let loop_info = LoopInfo::new(&cfi, &dominators); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + v0:Any = Const Value(false) + IfTrue v0, bb1() + Jump bb3() + bb1(): + Jump bb2() + bb2(): + IfTrue v0, bb1() + Jump bb5() + bb3(): + Jump bb4() + bb4(): + IfTrue v0, bb3() + Jump bb5() + bb5(): + IfTrue v0, bb0() + Jump bb6() + bb6(): + v11:Any = Const CBool(true) + Return v11 + "); + + assert!(loop_info.is_loop_header(bb0)); + assert!(loop_info.is_loop_header(bb1)); + assert!(!loop_info.is_loop_header(bb2)); + assert!(loop_info.is_loop_header(bb3)); + assert!(!loop_info.is_loop_header(bb5)); + assert!(!loop_info.is_loop_header(bb4)); + assert!(!loop_info.is_loop_header(bb6)); + + assert_eq!(loop_info.loop_depth(bb0), 1); + assert_eq!(loop_info.loop_depth(bb1), 2); + assert_eq!(loop_info.loop_depth(bb2), 2); + assert_eq!(loop_info.loop_depth(bb3), 2); + assert_eq!(loop_info.loop_depth(bb4), 2); + assert_eq!(loop_info.loop_depth(bb5), 1); + assert_eq!(loop_info.loop_depth(bb6), 0); + + assert!(loop_info.is_back_edge_source(bb2)); + assert!(loop_info.is_back_edge_source(bb4)); + assert!(loop_info.is_back_edge_source(bb5)); + } + + #[test] + fn linked_list_non_loop() { + // ┌─────┐ + // │ bb0 │ + // └──┬──┘ + // │ + // ┌──▼──┐ + // │ bb1 │ + // └──┬──┘ + // │ + // ┌──▼──┐ + // │ bb2 │ + // └─────┘ + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + + let _ = function.push_insn(bb0, Insn::Jump(edge(bb1))); + let _ = function.push_insn(bb1, Insn::Jump(edge(bb2))); + + let retval = function.push_insn(bb2, Insn::Const { val: Const::CBool(true) }); + let _ = function.push_insn(bb2, Insn::Return { val: retval }); + + let cfi = ControlFlowInfo::new(&function); + let dominators = Dominators::new(&function); + let loop_info = LoopInfo::new(&cfi, &dominators); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + Jump bb1() + bb1(): + Jump bb2() + bb2(): + v2:Any = Const CBool(true) + Return v2 + "); + + assert!(!loop_info.is_loop_header(bb0)); + assert!(!loop_info.is_loop_header(bb1)); + assert!(!loop_info.is_loop_header(bb2)); + + assert!(!loop_info.is_back_edge_source(bb0)); + assert!(!loop_info.is_back_edge_source(bb1)); + assert!(!loop_info.is_back_edge_source(bb2)); + + assert_eq!(loop_info.loop_depth(bb0), 0); + assert_eq!(loop_info.loop_depth(bb1), 0); + assert_eq!(loop_info.loop_depth(bb2), 0); + } + + #[test] + fn triple_nested_loop() { + // ┌─────┐ + // │ bb0 ◄──┐ + // └──┬──┘ │ + // │ │ + // ┌──▼──┐ │ + // │ bb1 ◄─┐│ + // └──┬──┘ ││ + // │ ││ + // ┌──▼──┐ ││ + // │ bb2 ◄┐││ + // └──┬──┘│││ + // │ │││ + // ┌──▼──┐│││ + // │ bb3 ┼┘││ + // └──┬──┘ ││ + // │ ││ + // ┌──▼──┐ ││ + // │ bb4 ┼─┘│ + // └──┬──┘ │ + // │ │ + // ┌──▼──┐ │ + // │ bb5 ┼──┘ + // └─────┘ + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + let bb4 = function.new_block(0); + let bb5 = function.new_block(0); + + let cond = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb0, Insn::Jump(edge(bb1))); + let _ = function.push_insn(bb1, Insn::Jump(edge(bb2))); + let _ = function.push_insn(bb2, Insn::Jump(edge(bb3))); + let _ = function.push_insn(bb3, Insn::Jump(edge(bb4))); + let _ = function.push_insn(bb3, Insn::IfTrue {val: cond, target: edge(bb2)}); + let _ = function.push_insn(bb4, Insn::Jump(edge(bb5))); + let _ = function.push_insn(bb4, Insn::IfTrue {val: cond, target: edge(bb1)}); + let _ = function.push_insn(bb5, Insn::IfTrue {val: cond, target: edge(bb0)}); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + v0:Any = Const Value(false) + Jump bb1() + bb1(): + Jump bb2() + bb2(): + Jump bb3() + bb3(): + Jump bb4() + IfTrue v0, bb2() + bb4(): + Jump bb5() + IfTrue v0, bb1() + bb5(): + IfTrue v0, bb0() + "); + + let cfi = ControlFlowInfo::new(&function); + let dominators = Dominators::new(&function); + let loop_info = LoopInfo::new(&cfi, &dominators); + + assert!(!loop_info.is_back_edge_source(bb0)); + assert!(!loop_info.is_back_edge_source(bb1)); + assert!(!loop_info.is_back_edge_source(bb2)); + assert!(loop_info.is_back_edge_source(bb3)); + assert!(loop_info.is_back_edge_source(bb4)); + assert!(loop_info.is_back_edge_source(bb5)); + + assert_eq!(loop_info.loop_depth(bb0), 1); + assert_eq!(loop_info.loop_depth(bb1), 2); + assert_eq!(loop_info.loop_depth(bb2), 3); + assert_eq!(loop_info.loop_depth(bb3), 3); + assert_eq!(loop_info.loop_depth(bb4), 2); + assert_eq!(loop_info.loop_depth(bb5), 1); + + assert!(loop_info.is_loop_header(bb0)); + assert!(loop_info.is_loop_header(bb1)); + assert!(loop_info.is_loop_header(bb2)); + assert!(!loop_info.is_loop_header(bb3)); + assert!(!loop_info.is_loop_header(bb4)); + assert!(!loop_info.is_loop_header(bb5)); + } + } + +/// Test dumping to iongraph format. +#[cfg(test)] +mod iongraph_tests { + use super::*; + use insta::assert_snapshot; + + fn edge(target: BlockId) -> BranchEdge { + BranchEdge { target, args: vec![] } + } + + #[test] + fn test_simple_function() { + let mut function = Function::new(std::ptr::null()); + let bb0 = function.entry_block; + + let retval = function.push_insn(bb0, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb0, Insn::Return { val: retval }); + + let json = function.to_iongraph_pass("simple"); + assert_snapshot!(json.to_string(), @r#"{"name":"simple", "mir":{"blocks":[{"ptr":4096, "id":0, "loopDepth":0, "attributes":[], "predecessors":[], "successors":[], "instructions":[{"ptr":4096, "id":0, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4097, "id":1, "opcode":"Return v0", "attributes":[], "inputs":[0], "uses":[], "memInputs":[], "type":""}]}]}, "lir":{"blocks":[]}}"#); + } + + #[test] + fn test_two_blocks() { + let mut function = Function::new(std::ptr::null()); + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb1))); + + let retval = function.push_insn(bb1, Insn::Const { val: Const::CBool(false) }); + function.push_insn(bb1, Insn::Return { val: retval }); + + let json = function.to_iongraph_pass("two_blocks"); + assert_snapshot!(json.to_string(), @r#"{"name":"two_blocks", "mir":{"blocks":[{"ptr":4096, "id":0, "loopDepth":0, "attributes":[], "predecessors":[], "successors":[1], "instructions":[{"ptr":4096, "id":0, "opcode":"Jump bb1()", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":""}]}, {"ptr":4097, "id":1, "loopDepth":0, "attributes":[], "predecessors":[0], "successors":[], "instructions":[{"ptr":4097, "id":1, "opcode":"Const CBool(false)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4098, "id":2, "opcode":"Return v1", "attributes":[], "inputs":[1], "uses":[], "memInputs":[], "type":""}]}]}, "lir":{"blocks":[]}}"#); + } + + #[test] + fn test_multiple_instructions() { + let mut function = Function::new(std::ptr::null()); + let bb0 = function.entry_block; + + let val1 = function.push_insn(bb0, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb0, Insn::Return { val: val1 }); + + let json = function.to_iongraph_pass("multiple_instructions"); + assert_snapshot!(json.to_string(), @r#"{"name":"multiple_instructions", "mir":{"blocks":[{"ptr":4096, "id":0, "loopDepth":0, "attributes":[], "predecessors":[], "successors":[], "instructions":[{"ptr":4096, "id":0, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4097, "id":1, "opcode":"Return v0", "attributes":[], "inputs":[0], "uses":[], "memInputs":[], "type":""}]}]}, "lir":{"blocks":[]}}"#); + } + + #[test] + fn test_conditional_branch() { + let mut function = Function::new(std::ptr::null()); + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + + let cond = function.push_insn(bb0, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb0, Insn::IfTrue { val: cond, target: edge(bb1) }); + + let retval1 = function.push_insn(bb0, Insn::Const { val: Const::CBool(false) }); + function.push_insn(bb0, Insn::Return { val: retval1 }); + + let retval2 = function.push_insn(bb1, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb1, Insn::Return { val: retval2 }); + + let json = function.to_iongraph_pass("conditional_branch"); + assert_snapshot!(json.to_string(), @r#"{"name":"conditional_branch", "mir":{"blocks":[{"ptr":4096, "id":0, "loopDepth":0, "attributes":[], "predecessors":[], "successors":[1], "instructions":[{"ptr":4096, "id":0, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4097, "id":1, "opcode":"IfTrue v0, bb1()", "attributes":[], "inputs":[0], "uses":[], "memInputs":[], "type":""}, {"ptr":4098, "id":2, "opcode":"Const CBool(false)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4099, "id":3, "opcode":"Return v2", "attributes":[], "inputs":[2], "uses":[], "memInputs":[], "type":""}]}, {"ptr":4097, "id":1, "loopDepth":0, "attributes":[], "predecessors":[0], "successors":[], "instructions":[{"ptr":4100, "id":4, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4101, "id":5, "opcode":"Return v4", "attributes":[], "inputs":[4], "uses":[], "memInputs":[], "type":""}]}]}, "lir":{"blocks":[]}}"#); + } + + #[test] + fn test_loop_structure() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb2))); + + let val = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb2, Insn::IfTrue { val, target: edge(bb1)}); + let retval = function.push_insn(bb2, Insn::Const { val: Const::CBool(true) }); + let _ = function.push_insn(bb2, Insn::Return { val: retval }); + + function.push_insn(bb1, Insn::Jump(edge(bb2))); + + let json = function.to_iongraph_pass("loop_structure"); + assert_snapshot!(json.to_string(), @r#"{"name":"loop_structure", "mir":{"blocks":[{"ptr":4096, "id":0, "loopDepth":0, "attributes":[], "predecessors":[], "successors":[2], "instructions":[{"ptr":4096, "id":0, "opcode":"Jump bb2()", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":""}, {"ptr":4097, "id":1, "opcode":"Const Value(false)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}]}, {"ptr":4098, "id":2, "loopDepth":1, "attributes":["loopheader"], "predecessors":[0, 1], "successors":[1], "instructions":[{"ptr":4098, "id":2, "opcode":"IfTrue v1, bb1()", "attributes":[], "inputs":[1], "uses":[], "memInputs":[], "type":""}, {"ptr":4099, "id":3, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4100, "id":4, "opcode":"Return v3", "attributes":[], "inputs":[3], "uses":[], "memInputs":[], "type":""}]}, {"ptr":4097, "id":1, "loopDepth":1, "attributes":["backedge"], "predecessors":[2], "successors":[2], "instructions":[{"ptr":4101, "id":5, "opcode":"Jump bb2()", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":""}]}]}, "lir":{"blocks":[]}}"#); + } + + #[test] + fn test_multiple_successors() { + let mut function = Function::new(std::ptr::null()); + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + + let cond = function.push_insn(bb0, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb0, Insn::IfTrue { val: cond, target: edge(bb1) }); + function.push_insn(bb0, Insn::Jump(edge(bb2))); + + let retval1 = function.push_insn(bb1, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb1, Insn::Return { val: retval1 }); + + let retval2 = function.push_insn(bb2, Insn::Const { val: Const::CBool(false) }); + function.push_insn(bb2, Insn::Return { val: retval2 }); + + let json = function.to_iongraph_pass("multiple_successors"); + assert_snapshot!(json.to_string(), @r#"{"name":"multiple_successors", "mir":{"blocks":[{"ptr":4096, "id":0, "loopDepth":0, "attributes":[], "predecessors":[], "successors":[1, 2], "instructions":[{"ptr":4096, "id":0, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4097, "id":1, "opcode":"IfTrue v0, bb1()", "attributes":[], "inputs":[0], "uses":[], "memInputs":[], "type":""}, {"ptr":4098, "id":2, "opcode":"Jump bb2()", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":""}]}, {"ptr":4097, "id":1, "loopDepth":0, "attributes":[], "predecessors":[0], "successors":[], "instructions":[{"ptr":4099, "id":3, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4100, "id":4, "opcode":"Return v3", "attributes":[], "inputs":[3], "uses":[], "memInputs":[], "type":""}]}, {"ptr":4098, "id":2, "loopDepth":0, "attributes":[], "predecessors":[0], "successors":[], "instructions":[{"ptr":4101, "id":5, "opcode":"Const CBool(false)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4102, "id":6, "opcode":"Return v5", "attributes":[], "inputs":[5], "uses":[], "memInputs":[], "type":""}]}]}, "lir":{"blocks":[]}}"#); + } + } diff --git a/zjit/src/options.rs b/zjit/src/options.rs index c165035eaa1af0..b7e2c71cefcd65 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -70,6 +70,9 @@ pub struct Options { /// Dump High-level IR to the given file in Graphviz format after optimization pub dump_hir_graphviz: Option, + /// Dump High-level IR in Iongraph JSON format after optimization to /tmp/zjit-iongraph-{$PID} + pub dump_hir_iongraph: bool, + /// Dump low-level IR pub dump_lir: Option>, @@ -106,6 +109,7 @@ impl Default for Options { dump_hir_init: None, dump_hir_opt: None, dump_hir_graphviz: None, + dump_hir_iongraph: false, dump_lir: None, dump_disasm: false, trace_side_exits: None, @@ -353,6 +357,8 @@ fn parse_option(str_ptr: *const std::os::raw::c_char) -> Option<()> { options.dump_hir_graphviz = Some(opt_val); } + ("dump-hir-iongraph", "") => options.dump_hir_iongraph = true, + ("dump-lir", "") => options.dump_lir = Some(HashSet::from([DumpLIR::init])), ("dump-lir", filters) => { let mut dump_lirs = HashSet::new(); diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index 47bae3ac633a86..8c8190609d7fae 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -361,3 +361,17 @@ impl IseqProfile { } } } + +#[cfg(test)] +mod tests { + use crate::cruby::*; + + #[test] + fn can_profile_block_handler() { + with_rubyvm(|| eval(" + def foo = yield + foo rescue 0 + foo rescue 0 + ")); + } +} diff --git a/zjit/src/state.rs b/zjit/src/state.rs index 06296eb8f20d08..fd59161812a7ee 100644 --- a/zjit/src/state.rs +++ b/zjit/src/state.rs @@ -59,6 +59,9 @@ pub struct ZJITState { /// Counter pointers for un-annotated C functions not_annotated_frame_cfunc_counter_pointers: HashMap>, + /// Counter pointers for all calls to any kind of C function from JIT code + ccall_counter_pointers: HashMap>, + /// Locations of side exists within generated code exit_locations: Option, } @@ -135,6 +138,7 @@ impl ZJITState { exit_trampoline_with_counter: exit_trampoline, full_frame_cfunc_counter_pointers: HashMap::new(), not_annotated_frame_cfunc_counter_pointers: HashMap::new(), + ccall_counter_pointers: HashMap::new(), exit_locations, }; unsafe { ZJIT_STATE = Enabled(zjit_state); } @@ -215,6 +219,11 @@ impl ZJITState { &mut ZJITState::get_instance().not_annotated_frame_cfunc_counter_pointers } + /// Get a mutable reference to ccall counter pointers + pub fn get_ccall_counter_pointers() -> &'static mut HashMap> { + &mut ZJITState::get_instance().ccall_counter_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 b0ca28d258506a..df172997ce9793 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -689,6 +689,13 @@ pub extern "C" fn rb_zjit_stats(_ec: EcPtr, _self: VALUE, target_key: VALUE) -> set_stat_usize!(hash, &key_string, **counter); } + // Set ccall counters + let ccall = ZJITState::get_ccall_counter_pointers(); + for (signature, counter) in ccall.iter() { + let key_string = format!("ccall_{}", signature); + set_stat_usize!(hash, &key_string, **counter); + } + hash }