ZJIT: Clean up partial SSI (#15929)

After Kokubun requested named unions, I realized we don't actually need
a `Type::subtract` function. They were only used for the ad-hoc unions.

Also, add a test that is illustrative of what we can get from this
partial SSI.
This commit is contained in:
Max Bernstein 2026-01-22 11:41:23 -05:00 committed by GitHub
parent 61724b9e4a
commit a7a0c36b20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
Notes: git 2026-01-22 16:41:55 +00:00
Merged-By: tekknolagi <donotemailthisaddress@bernsteinbear.com>
4 changed files with 117 additions and 65 deletions

View File

@ -6293,7 +6293,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
let test_id = fun.push_insn(block, Insn::Test { val });
let target_idx = insn_idx_at_offset(insn_idx, offset);
let target = insn_idx_to_block[&target_idx];
let nil_false_type = types::NilClass.union(types::FalseClass);
let nil_false_type = types::Falsy;
let nil_false = fun.push_insn(block, Insn::RefineType { val, new_type: nil_false_type });
let mut iffalse_state = state.clone();
iffalse_state.replace(val, nil_false);
@ -6301,7 +6301,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
val: test_id,
target: BranchEdge { target, args: iffalse_state.as_args(self_param) }
});
let not_nil_false_type = types::BasicObject.subtract(types::NilClass).subtract(types::FalseClass);
let not_nil_false_type = types::Truthy;
let not_nil_false = fun.push_insn(block, Insn::RefineType { val, new_type: not_nil_false_type });
state.replace(val, not_nil_false);
queue.push_back((state.clone(), target, target_idx, local_inval));
@ -6313,7 +6313,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
let test_id = fun.push_insn(block, Insn::Test { val });
let target_idx = insn_idx_at_offset(insn_idx, offset);
let target = insn_idx_to_block[&target_idx];
let not_nil_false_type = types::BasicObject.subtract(types::NilClass).subtract(types::FalseClass);
let not_nil_false_type = types::Truthy;
let not_nil_false = fun.push_insn(block, Insn::RefineType { val, new_type: not_nil_false_type });
let mut iftrue_state = state.clone();
iftrue_state.replace(val, not_nil_false);
@ -6321,7 +6321,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
val: test_id,
target: BranchEdge { target, args: iftrue_state.as_args(self_param) }
});
let nil_false_type = types::NilClass.union(types::FalseClass);
let nil_false_type = types::Falsy;
let nil_false = fun.push_insn(block, Insn::RefineType { val, new_type: nil_false_type });
state.replace(val, nil_false);
queue.push_back((state.clone(), target, target_idx, local_inval));
@ -6340,7 +6340,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
val: test_id,
target: BranchEdge { target, args: iftrue_state.as_args(self_param) }
});
let new_type = types::BasicObject.subtract(types::NilClass);
let new_type = types::NotNil;
let not_nil = fun.push_insn(block, Insn::RefineType { val, new_type });
state.replace(val, not_nil);
queue.push_back((state.clone(), target, target_idx, local_inval));

View File

@ -11319,4 +11319,53 @@ mod hir_opt_tests {
Return v31
");
}
#[test]
fn test_infer_truthiness_from_branch() {
eval("
def test(x)
if x
if x
if x
3
else
4
end
else
5
end
else
6
end
end
");
assert_snapshot!(hir_string("test"), @r"
fn test@<compiled>:3:
bb0():
EntryPoint interpreter
v1:BasicObject = LoadSelf
v2:BasicObject = GetLocal :x, l0, SP@4
Jump bb2(v1, v2)
bb1(v5:BasicObject, v6:BasicObject):
EntryPoint JIT(0)
Jump bb2(v5, v6)
bb2(v8:BasicObject, v9:BasicObject):
CheckInterrupts
v15:CBool = Test v9
v16:Falsy = RefineType v9, Falsy
IfFalse v15, bb5(v8, v16)
v18:Truthy = RefineType v9, Truthy
CheckInterrupts
v26:Truthy = RefineType v18, Truthy
CheckInterrupts
v34:Truthy = RefineType v26, Truthy
v37:Fixnum[3] = Const Value(3)
CheckInterrupts
Return v37
bb5(v42:BasicObject, v43:Falsy):
v47:Fixnum[6] = Const Value(6)
CheckInterrupts
Return v47
");
}
}

View File

@ -3222,6 +3222,69 @@ pub mod hir_build_tests {
");
}
#[test]
fn test_infer_truthiness_from_branch() {
eval("
def test(x)
if x
if x
if x
3
else
4
end
else
5
end
else
6
end
end
");
assert_snapshot!(hir_string("test"), @r"
fn test@<compiled>:3:
bb0():
EntryPoint interpreter
v1:BasicObject = LoadSelf
v2:BasicObject = GetLocal :x, l0, SP@4
Jump bb2(v1, v2)
bb1(v5:BasicObject, v6:BasicObject):
EntryPoint JIT(0)
Jump bb2(v5, v6)
bb2(v8:BasicObject, v9:BasicObject):
CheckInterrupts
v15:CBool = Test v9
v16:Falsy = RefineType v9, Falsy
IfFalse v15, bb5(v8, v16)
v18:Truthy = RefineType v9, Truthy
CheckInterrupts
v23:CBool[true] = Test v18
v24 = RefineType v18, Falsy
IfFalse v23, bb4(v8, v24)
v26:Truthy = RefineType v18, Truthy
CheckInterrupts
v31:CBool[true] = Test v26
v32 = RefineType v26, Falsy
IfFalse v31, bb3(v8, v32)
v34:Truthy = RefineType v26, Truthy
v37:Fixnum[3] = Const Value(3)
CheckInterrupts
Return v37
bb5(v42:BasicObject, v43:Falsy):
v47:Fixnum[6] = Const Value(6)
CheckInterrupts
Return v47
bb4(v52, v53):
v57 = Const Value(5)
CheckInterrupts
Return v57
bb3(v62, v63):
v67 = Const Value(4)
CheckInterrupts
Return v67
");
}
#[test]
fn test_invokebuiltin_delegate_annotated() {
assert_contains_opcode("Float", YARVINSN_opt_invokebuiltin_delegate_leave);

View File

@ -453,25 +453,6 @@ impl Type {
types::Empty
}
/// Subtract `other` from `self`, preserving specialization if possible.
pub fn subtract(&self, other: Type) -> Type {
// If self is a subtype of other, the result is empty (no negative types).
if self.is_subtype(other) { return types::Empty; }
// Self is not a subtype of other. That means either:
// * Their type bits do not overlap at all (eg Int vs String)
// * Their type bits overlap but self's specialization is not a subtype of other's (eg
// Fixnum[5] vs Fixnum[4])
// Check for the latter case, returning self unchanged if so.
if !self.spec_is_subtype_of(other) {
return *self;
}
// Now self is either a supertype of other (eg Object vs String or Fixnum vs Fixnum[5]) or
// their type bits do not overlap at all (eg Int vs String).
// Just subtract the bits and keep self's specialization.
let bits = self.bits & !other.bits;
Type { bits, spec: self.spec }
}
pub fn could_be(&self, other: Type) -> bool {
!self.intersection(other).bit_equal(types::Empty)
}
@ -1079,45 +1060,4 @@ mod tests {
assert!(!types::CBool.has_value(Const::CBool(true)));
assert!(!types::CShape.has_value(Const::CShape(crate::cruby::ShapeId(0x1234))));
}
#[test]
fn test_subtract_with_superset_returns_empty() {
let left = types::NilClass;
let right = types::BasicObject;
let result = left.subtract(right);
assert_bit_equal(result, types::Empty);
}
#[test]
fn test_subtract_with_subset_removes_bits() {
let left = types::BasicObject;
let right = types::NilClass;
let result = left.subtract(right);
assert_subtype(result, types::BasicObject);
assert_not_subtype(types::NilClass, result);
}
#[test]
fn test_subtract_with_no_overlap_returns_self() {
let left = types::Fixnum;
let right = types::StringExact;
let result = left.subtract(right);
assert_bit_equal(result, left);
}
#[test]
fn test_subtract_with_no_specialization_overlap_returns_self() {
let left = Type::fixnum(4);
let right = Type::fixnum(5);
let result = left.subtract(right);
assert_bit_equal(result, left);
}
#[test]
fn test_subtract_with_specialization_subset_removes_specialization() {
let left = types::Fixnum;
let right = Type::fixnum(42);
let result = left.subtract(right);
assert_bit_equal(result, types::Fixnum);
}
}