-
Notifications
You must be signed in to change notification settings - Fork 12.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ADT] Specialize ValueIsPresent for PointerUnion #121847
base: main
Are you sure you want to change the base?
Conversation
Currently, two instances of `PointerUnion` with different active members and null value compare unequal. In some cases, this results in counterintuitive behavior when using functions from `Casting.h`, e.g.: ``` PointerUnion<int *, float *> U; // U = (int *)nullptr; dyn_cast<int *>(U); // Aborts dyn_cast<float *>(U); // Aborts U = (float *)nullptr; dyn_cast<int *>(U); // OK dyn_cast<float *>(U); // OK ``` `dyn_cast` should abort in all cases because the argument is null. Currently, it aborts only if the first member is active. This happens because the partial template specialization of `ValueIsPresent` for nullable types compares the union with a union constructed from nullptr, and the two unions compare equal only if their active members are the same. This patch makes two instances of a union compare equal if they are both null regardless of their active members, and fixes two places where the old behavior was exploited.
@llvm/pr-subscribers-llvm-support @llvm/pr-subscribers-backend-amdgpu Author: Sergei Barannikov (s-barannikov) ChangesCurrently, two instances of PointerUnion<int *, float *> U;
// U = (int *)nullptr;
dyn_cast<int *>(U); // Aborts
dyn_cast<float *>(U); // Aborts
U = (float *)nullptr;
dyn_cast<int *>(U); // OK
dyn_cast<float *>(U); // OK
This patch makes two instances of a union compare equal if they are both null regardless of their active members, and fixes two places where the old behavior was exploited. Full diff: https://github.com/llvm/llvm-project/pull/121847.diff 4 Files Affected:
diff --git a/llvm/include/llvm/ADT/PointerUnion.h b/llvm/include/llvm/ADT/PointerUnion.h
index 7d4ed02b622626..bea9d6a4569b0b 100644
--- a/llvm/include/llvm/ADT/PointerUnion.h
+++ b/llvm/include/llvm/ADT/PointerUnion.h
@@ -198,14 +198,14 @@ class PointerUnion
}
};
-template <typename ...PTs>
-bool operator==(PointerUnion<PTs...> lhs, PointerUnion<PTs...> rhs) {
- return lhs.getOpaqueValue() == rhs.getOpaqueValue();
+template <typename... PTs>
+bool operator==(PointerUnion<PTs...> LHS, PointerUnion<PTs...> RHS) {
+ return (!LHS && !RHS) || LHS.getOpaqueValue() == RHS.getOpaqueValue();
}
-template <typename ...PTs>
-bool operator!=(PointerUnion<PTs...> lhs, PointerUnion<PTs...> rhs) {
- return lhs.getOpaqueValue() != rhs.getOpaqueValue();
+template <typename... PTs>
+bool operator!=(PointerUnion<PTs...> LHS, PointerUnion<PTs...> RHS) {
+ return !operator==(LHS, RHS);
}
template <typename ...PTs>
diff --git a/llvm/lib/CodeGen/RegisterBankInfo.cpp b/llvm/lib/CodeGen/RegisterBankInfo.cpp
index e1720b038e2361..5a8cf13ad11fd5 100644
--- a/llvm/lib/CodeGen/RegisterBankInfo.cpp
+++ b/llvm/lib/CodeGen/RegisterBankInfo.cpp
@@ -134,10 +134,10 @@ const TargetRegisterClass *RegisterBankInfo::constrainGenericRegister(
// If the register already has a class, fallback to MRI::constrainRegClass.
auto &RegClassOrBank = MRI.getRegClassOrRegBank(Reg);
- if (isa<const TargetRegisterClass *>(RegClassOrBank))
+ if (isa_and_present<const TargetRegisterClass *>(RegClassOrBank))
return MRI.constrainRegClass(Reg, &RC);
- const RegisterBank *RB = cast<const RegisterBank *>(RegClassOrBank);
+ const auto *RB = dyn_cast_if_present<const RegisterBank *>(RegClassOrBank);
// Otherwise, all we can do is ensure the bank covers the class, and set it.
if (RB && !RB->covers(RC))
return nullptr;
diff --git a/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp b/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp
index 704435dad65d7b..8fa656c77e90ed 100644
--- a/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp
+++ b/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp
@@ -3708,10 +3708,10 @@ const TargetRegisterClass *
SIRegisterInfo::getConstrainedRegClassForOperand(const MachineOperand &MO,
const MachineRegisterInfo &MRI) const {
const RegClassOrRegBank &RCOrRB = MRI.getRegClassOrRegBank(MO.getReg());
- if (const RegisterBank *RB = dyn_cast<const RegisterBank *>(RCOrRB))
+ if (const auto *RB = dyn_cast_if_present<const RegisterBank *>(RCOrRB))
return getRegClassForTypeOnBank(MRI.getType(MO.getReg()), *RB);
- if (const auto *RC = dyn_cast<const TargetRegisterClass *>(RCOrRB))
+ if (const auto *RC = dyn_cast_if_present<const TargetRegisterClass *>(RCOrRB))
return getAllocatableClass(RC);
return nullptr;
diff --git a/llvm/unittests/ADT/PointerUnionTest.cpp b/llvm/unittests/ADT/PointerUnionTest.cpp
index acddb789601494..d870339b01397d 100644
--- a/llvm/unittests/ADT/PointerUnionTest.cpp
+++ b/llvm/unittests/ADT/PointerUnionTest.cpp
@@ -53,9 +53,16 @@ TEST_F(PointerUnionTest, Comparison) {
EXPECT_TRUE(i4 != l4);
EXPECT_TRUE(f4 != l4);
EXPECT_TRUE(l4 != d4);
- EXPECT_TRUE(i4null != f4null);
- EXPECT_TRUE(i4null != l4null);
- EXPECT_TRUE(i4null != d4null);
+ EXPECT_TRUE(i4null == f4null);
+ EXPECT_FALSE(i4null != f4null);
+ EXPECT_TRUE(i4null == l4null);
+ EXPECT_FALSE(i4null != l4null);
+ EXPECT_TRUE(i4null == d4null);
+ EXPECT_FALSE(i4null != d4null);
+ EXPECT_FALSE(i4null == i4);
+ EXPECT_TRUE(i4null != i4);
+ EXPECT_FALSE(i4null == f4);
+ EXPECT_TRUE(i4null != f4);
}
TEST_F(PointerUnionTest, Null) {
|
@llvm/pr-subscribers-llvm-regalloc Author: Sergei Barannikov (s-barannikov) ChangesCurrently, two instances of PointerUnion<int *, float *> U;
// U = (int *)nullptr;
dyn_cast<int *>(U); // Aborts
dyn_cast<float *>(U); // Aborts
U = (float *)nullptr;
dyn_cast<int *>(U); // OK
dyn_cast<float *>(U); // OK
This patch makes two instances of a union compare equal if they are both null regardless of their active members, and fixes two places where the old behavior was exploited. Full diff: https://github.com/llvm/llvm-project/pull/121847.diff 4 Files Affected:
diff --git a/llvm/include/llvm/ADT/PointerUnion.h b/llvm/include/llvm/ADT/PointerUnion.h
index 7d4ed02b622626..bea9d6a4569b0b 100644
--- a/llvm/include/llvm/ADT/PointerUnion.h
+++ b/llvm/include/llvm/ADT/PointerUnion.h
@@ -198,14 +198,14 @@ class PointerUnion
}
};
-template <typename ...PTs>
-bool operator==(PointerUnion<PTs...> lhs, PointerUnion<PTs...> rhs) {
- return lhs.getOpaqueValue() == rhs.getOpaqueValue();
+template <typename... PTs>
+bool operator==(PointerUnion<PTs...> LHS, PointerUnion<PTs...> RHS) {
+ return (!LHS && !RHS) || LHS.getOpaqueValue() == RHS.getOpaqueValue();
}
-template <typename ...PTs>
-bool operator!=(PointerUnion<PTs...> lhs, PointerUnion<PTs...> rhs) {
- return lhs.getOpaqueValue() != rhs.getOpaqueValue();
+template <typename... PTs>
+bool operator!=(PointerUnion<PTs...> LHS, PointerUnion<PTs...> RHS) {
+ return !operator==(LHS, RHS);
}
template <typename ...PTs>
diff --git a/llvm/lib/CodeGen/RegisterBankInfo.cpp b/llvm/lib/CodeGen/RegisterBankInfo.cpp
index e1720b038e2361..5a8cf13ad11fd5 100644
--- a/llvm/lib/CodeGen/RegisterBankInfo.cpp
+++ b/llvm/lib/CodeGen/RegisterBankInfo.cpp
@@ -134,10 +134,10 @@ const TargetRegisterClass *RegisterBankInfo::constrainGenericRegister(
// If the register already has a class, fallback to MRI::constrainRegClass.
auto &RegClassOrBank = MRI.getRegClassOrRegBank(Reg);
- if (isa<const TargetRegisterClass *>(RegClassOrBank))
+ if (isa_and_present<const TargetRegisterClass *>(RegClassOrBank))
return MRI.constrainRegClass(Reg, &RC);
- const RegisterBank *RB = cast<const RegisterBank *>(RegClassOrBank);
+ const auto *RB = dyn_cast_if_present<const RegisterBank *>(RegClassOrBank);
// Otherwise, all we can do is ensure the bank covers the class, and set it.
if (RB && !RB->covers(RC))
return nullptr;
diff --git a/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp b/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp
index 704435dad65d7b..8fa656c77e90ed 100644
--- a/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp
+++ b/llvm/lib/Target/AMDGPU/SIRegisterInfo.cpp
@@ -3708,10 +3708,10 @@ const TargetRegisterClass *
SIRegisterInfo::getConstrainedRegClassForOperand(const MachineOperand &MO,
const MachineRegisterInfo &MRI) const {
const RegClassOrRegBank &RCOrRB = MRI.getRegClassOrRegBank(MO.getReg());
- if (const RegisterBank *RB = dyn_cast<const RegisterBank *>(RCOrRB))
+ if (const auto *RB = dyn_cast_if_present<const RegisterBank *>(RCOrRB))
return getRegClassForTypeOnBank(MRI.getType(MO.getReg()), *RB);
- if (const auto *RC = dyn_cast<const TargetRegisterClass *>(RCOrRB))
+ if (const auto *RC = dyn_cast_if_present<const TargetRegisterClass *>(RCOrRB))
return getAllocatableClass(RC);
return nullptr;
diff --git a/llvm/unittests/ADT/PointerUnionTest.cpp b/llvm/unittests/ADT/PointerUnionTest.cpp
index acddb789601494..d870339b01397d 100644
--- a/llvm/unittests/ADT/PointerUnionTest.cpp
+++ b/llvm/unittests/ADT/PointerUnionTest.cpp
@@ -53,9 +53,16 @@ TEST_F(PointerUnionTest, Comparison) {
EXPECT_TRUE(i4 != l4);
EXPECT_TRUE(f4 != l4);
EXPECT_TRUE(l4 != d4);
- EXPECT_TRUE(i4null != f4null);
- EXPECT_TRUE(i4null != l4null);
- EXPECT_TRUE(i4null != d4null);
+ EXPECT_TRUE(i4null == f4null);
+ EXPECT_FALSE(i4null != f4null);
+ EXPECT_TRUE(i4null == l4null);
+ EXPECT_FALSE(i4null != l4null);
+ EXPECT_TRUE(i4null == d4null);
+ EXPECT_FALSE(i4null != d4null);
+ EXPECT_FALSE(i4null == i4);
+ EXPECT_TRUE(i4null != i4);
+ EXPECT_FALSE(i4null == f4);
+ EXPECT_TRUE(i4null != f4);
}
TEST_F(PointerUnionTest, Null) {
|
@@ -134,10 +134,10 @@ const TargetRegisterClass *RegisterBankInfo::constrainGenericRegister( | |||
|
|||
// If the register already has a class, fallback to MRI::constrainRegClass. | |||
auto &RegClassOrBank = MRI.getRegClassOrRegBank(Reg); | |||
if (isa<const TargetRegisterClass *>(RegClassOrBank)) | |||
if (isa_and_present<const TargetRegisterClass *>(RegClassOrBank)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think either of these 2 instances should ever encounter a register without a set class or bank, this is papering over a different bug?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Input gMIR to instruction selector shouldn't contain registers without class/bank.
Such registers are created during instruction selection if an imported SelectionDAG pattern contains several instructions in the "destination DAG" of the pattern:
def : GCNPat <
(UniformUnaryFrag<fabs> (v2f16 SReg_32:$src)),
(S_AND_B32 SReg_32:$src, (S_MOV_B32 (i32 0x7fff7fff)))
>;
This is what -gen-global-isel
generates for this pattern:
GIR_MakeTempReg, /*TempRegID*/0, /*TypeID*/GILLT_s1,
GIR_BuildMI, /*InsnID*/1, /*Opcode*/GIMT_Encode2(AMDGPU::S_MOV_B32),
GIR_AddTempRegister, /*InsnID*/1, /*TempRegID*/0, /*TempRegFlags*/GIMT_Encode2(RegState::Define),
...
GIR_ConstrainSelectedInstOperands, /*InsnID*/1,
GIR_BuildRootMI, /*Opcode*/GIMT_Encode2(AMDGPU::S_AND_B32),
...
GIR_AddSimpleTempRegister, /*InsnID*/0, /*TempRegID*/0,
GIR_RootConstrainSelectedInstOperands,
GIR_MakeTempReg
creates a register without class/bank for the result of the S_MOV_B32
. The register gets its class when executing GIR_ConstrainSelectedInstOperands
action, which calls this function, which calls MRI.setRegClass()
at the end.
I don't know if this should be considered a bug. If it should, I can try to address it separately (probably in #121270).
(Unrelated to this PR). Note that the type of the temporary register is s1
. It is chosen arbitrarily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GIR_MakeTempReg creates a register without class/bank for the result of the S_MOV_B32.
As I mentioned in the other PR this is broken. In no context should an incomplete virtual register be used by an instruction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears there is another context when class/bank may not be set: 7e1f66d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After regbankselect / in the selection pass, there must be a class or bank set. The null/null case is only valid before that, when a generic vreg must have a type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I'm not really convinced this change is right. If PointerUnion allows having null pointers of different types, then treating them as equal may not be correct in general.
I think the fix here should either be that assigning null pointer that is not nullptr_t to a PointerUnion is invalid, or that dyn_cast_if_present should account for the different null pointers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems unusual / unexpected to me, Do we have an example of similar semantics in some other LLVM type or an external library?
Hm, I'm not really convinced this change is right. If PointerUnion allows having null pointers of different types, then treating them as equal may not be correct in general.
+1
I think I can agree with that. This makes sense if you think of
The assignment happens here. It does not call an assignment operator, it constructs a
I tried to specialize
which needs to be changed to something like: static inline bool isPresent(const T &t) { return static_cast<bool>(t); } |
|
b0a2b88
to
13a9098
Compare
EXPECT_FALSE(isa_and_present<int *>(i4null)); | ||
EXPECT_FALSE(isa_and_present<float *>(f4null)); | ||
EXPECT_FALSE(isa_and_present<long long *>(l4null)); | ||
EXPECT_FALSE(isa_and_present<double *>(d4null)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, only the first isa_and_present
returned false.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks much better to me than trying to relax equality comparisons
llvm/include/llvm/ADT/PointerUnion.h
Outdated
// Override the default behavior to return false for all possible null values. | ||
template <typename... PTs> | ||
struct ValueIsPresent<PointerUnion<PTs...>, | ||
std::enable_if_t<IsNullable<PointerUnion<PTs...>>>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this enable_if needed? Isn't PointerUnion always nullable because it satisfies std::is_constructible_v<PointerUnion<PTs...>, std::nullptr_t>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that this looks suspicious, but this is the only way I could get it compiled without errors.
I would appreciate it if someone could explain to me why this is necessary and how it can be simplified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the underlying issue is that without this, the main partial specialization from here would be as good of a match as this one:
static inline bool isPresent(const T &t) { return t != T(nullptr); } |
void
at the end both L622 and here. In the godbolt link, enable_if<true>
doesn't work because it appears in non-deduced context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another way to handle this would be to change the main ValueIsPresent
for nullable types to cast to bool instead, or introduce some traits for nullable types that would tell you how to check for null values that you can specialize for PointerUnion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IE I don't think the exact predicate used in this enable_if matters as long as it evaluates to
void
at the end both L622 and here. In the godbolt link,enable_if<true>
doesn't work because it appears in non-deduced context.
I tried
template <typename... PTs>
struct ValueIsPresent<PointerUnion<PTs...>, std::void_t<PTs...>>
which always evaluates to void
and introduces(?) deduced context, but the error persists. Shouldn't this work?
Another way to handle this would be to change the main
ValueIsPresent
for nullable types to cast to bool instead, or introduce some traits for nullable types that would tell you how to check for null values that you can specialize for PointerUnion.
The existing implementation implicitly assumes that if a type is constructible from nullptr_t
, then operator==
exists and works the way that's expected in this particular use case. Apparently, this doesn't work for PointerUnion.
Casting to bool works for PointerUnion, but may in theory not work for other types. I'll make this change because it is simpler, but I think the best solution would be to stop making implicit assumptions about existence / behavior of operator==
/ operator bool()
and require the clients to explicitly provide specializations if the behavior should diverge from the default.
@kuhar PTAL |
template <typename T> | ||
struct ValueIsPresent<T, std::enable_if_t<IsNullable<T>>> { | ||
using UnwrappedType = T; | ||
static inline bool isPresent(const T &t) { return t != T(nullptr); } | ||
static inline bool isPresent(const T &t) { return static_cast<bool>(t); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're going down this route, it would probably make sense to also change the enable_if to check std::is_convertible<T, bool> instead?
I think you could also drop the separate std::optional specialization because it also has operator bool.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're going down this route, it would probably make sense to also change the enable_if to check std::is_convertible<T, bool> instead?
I think it will make much more sense, will try.
I think you could also drop the separate std::optional specialization because it also has operator bool.
They are not quite the same, the specialization for std::optional
derefernces the argument of unwrapValue()
.
This can probably be guarded by constexpr if
, but personally I find the separate specialization clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears is_convertible
only handles implicit conversions, i.e. it returns false for types with explicit operator bool()
. I used is_constructible
with swapped arguments instead.
template <typename T> | ||
struct ValueIsPresent<T, std::enable_if_t<IsNullable<T>>> { | ||
struct ValueIsPresent<T, std::enable_if_t<std::is_constructible_v<bool, T>>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we keep the check that T can be constructed with nullptr
? There are types that can be cast to bool but are not pointer-like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about it, but figured that checking whether the type is convertible to bool exactly answers the question "does an object have a value". This may not be desired for non-class types (e.g. int
) but those shouldn't really be used in this context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this enable_if guards both isPresent
and unwrapValue
-- don't we need extra care for the latter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyway, I'm going to approve this as-is, use your judgement here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this enable_if guards both
isPresent
andunwrapValue
-- don't we need extra care for the latter?
Maybe, although constructibility from nullptr_t doesn't guarantee that the default implementation of unwrapValue would fit.
As I noted in the other comment, I think it would be best if we provided explicit specializations for each type like we do with simplify_type
, for example. Possibly provide a generic one for T *
, too. I would't commit myself to implementing this though, I've already shown my expertise in metaprogramming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unittests/Support/CMakeFiles/SupportTests.dir/Casting.cpp.o fails to build in pre-commit CI.
Now it cannot select a specialization for |
Two instances of
PointerUnion
with different active members and null value compare unequal. Currently, this results in counterintuitive behavior when using functions fromCasting.h
, e.g.:dyn_cast
should abort in all cases because the argument is null. Currently, it aborts only if the first member is active. This happens because the partial template specialization ofValueIsPresent
for nullable types compares the union with a union constructed from nullptr, and the two unions compare equal only if their active members are the same.This patch specializes
ValueIsPresent
further to makeisPresent()
return false for all possible null values, and fixes two places where the old behavior was exploited.