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
}