ZJIT: Support guarding *Exact types (#13797)

ZJIT already can generate guard type instructions for *Exact types.

For example:

```
def test(strings)
  strings.map do |string|
    string.bytesize
  end
end

test(["foo", "bar"])
```

```
HIR:
fn block in test:
bb0(v0:BasicObject, v1:BasicObject):
  PatchPoint MethodRedefined(String@0x1014be890, bytesize@0x19f1)
  v7:StringExact = GuardType v1, StringExact
  v8:Fixnum = CCall bytesize@0x16fa4cc18, v7
  Return v8

```

But zjit only supported guarding fixnums so this script would panic.

This commit adds support for guarding *Exact types.
This commit is contained in:
Stan Lo 2025-07-09 04:56:52 +01:00 committed by GitHub
parent 5aaedc052c
commit e9cd3060ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 203 additions and 0 deletions

View file

@ -915,6 +915,201 @@ class TestZJIT < Test::Unit::TestCase
end
end
def test_module_name_with_guard_passes
assert_compiles '"Integer"', %q{
def test(mod)
mod.name
end
test(String)
test(Integer)
}, call_threshold: 2
end
def test_module_name_with_guard_fallthrough
# This test demonstrates that the guard side exit works correctly
# In this case, when we call with a non-Class object, it should fall back to interpreter
assert_compiles '["String", "Integer", "Bar"]', %q{
class MyClass
def name = "Bar"
end
def test(mod)
mod.name
end
results = []
results << test(String)
results << test(Integer)
results << test(MyClass.new)
results
}, call_threshold: 2
end
def test_string_bytesize_with_guard
assert_compiles '5', %q{
def test(str)
str.bytesize
end
test('hello')
test('world')
}, call_threshold: 2
end
def test_nil_value_nil_opt_with_guard
assert_compiles 'true', %q{
def test(val) = val.nil?
test(nil)
test(nil)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_nil_value_nil_opt_with_guard_fallthrough
assert_compiles 'false', %q{
def test(val) = val.nil?
test(nil)
test(nil)
test(1)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_true_nil_opt_with_guard
assert_compiles 'false', %q{
def test(val) = val.nil?
test(true)
test(true)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_true_nil_opt_with_guard_fallthrough
assert_compiles 'true', %q{
def test(val) = val.nil?
test(true)
test(true)
test(nil)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_false_nil_opt_with_guard
assert_compiles 'false', %q{
def test(val) = val.nil?
test(false)
test(false)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_false_nil_opt_with_guard_fallthrough
assert_compiles 'true', %q{
def test(val) = val.nil?
test(false)
test(false)
test(nil)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_integer_nil_opt_with_guard
assert_compiles 'false', %q{
def test(val) = val.nil?
test(1)
test(2)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_integer_nil_opt_with_guard_fallthrough
assert_compiles 'true', %q{
def test(val) = val.nil?
test(1)
test(2)
test(nil)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_float_nil_opt_with_guard
assert_compiles 'false', %q{
def test(val) = val.nil?
test(1.0)
test(2.0)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_float_nil_opt_with_guard_fallthrough
assert_compiles 'true', %q{
def test(val) = val.nil?
test(1.0)
test(2.0)
test(nil)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_symbol_nil_opt_with_guard
assert_compiles 'false', %q{
def test(val) = val.nil?
test(:foo)
test(:bar)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_symbol_nil_opt_with_guard_fallthrough
assert_compiles 'true', %q{
def test(val) = val.nil?
test(:foo)
test(:bar)
test(nil)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_class_nil_opt_with_guard
assert_compiles 'false', %q{
def test(val) = val.nil?
test(String)
test(Integer)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_class_nil_opt_with_guard_fallthrough
assert_compiles 'true', %q{
def test(val) = val.nil?
test(String)
test(Integer)
test(nil)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_module_nil_opt_with_guard
assert_compiles 'false', %q{
def test(val) = val.nil?
test(Enumerable)
test(Kernel)
}, call_threshold: 2, insns: [:opt_nil_p]
end
def test_module_nil_opt_with_guard_fallthrough
assert_compiles 'true', %q{
def test(val) = val.nil?
test(Enumerable)
test(Kernel)
test(nil)
}, call_threshold: 2, insns: [:opt_nil_p]
end
private
# Assert that every method call in `test_script` can be compiled by ZJIT

View file

@ -974,6 +974,14 @@ fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, guard
// Check if opnd is Fixnum
asm.test(val, Opnd::UImm(RUBY_FIXNUM_FLAG as u64));
asm.jz(side_exit(jit, state)?);
} else if let Some(expected_class) = guard_type.runtime_exact_ruby_class() {
asm_comment!(asm, "guard exact class");
// Get the class of the value
let klass = asm.ccall(rb_yarv_class_of as *const u8, vec![val]);
asm.cmp(klass, Opnd::Value(expected_class));
asm.jne(side_exit(jit, state)?);
} else {
unimplemented!("unsupported type: {guard_type}");
}