Skip to content
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

[DAG] Add freeze(assertext(x)) -> assertext(x) folds #94491

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

bjope
Copy link
Collaborator

@bjope bjope commented Jun 5, 2024

This is a new attempt to allow pushing freeze through AssertSext/AssertZext in some situations.

An earlier attempt tried to solve this by handling AssertSext/AssertZext in canCreateUndefOrPoison. Simply saying that result of the assert nodes only are poison/undef if the input is poison/undef. That patch was reverted in
8505c3b

What happened was that we could end up doing folds such as
freeze(assertzext(x)) -> assertzext(freeze(x))
which isn't sound for operations that may "create" poison, unless dropping "poision generating flags" from the operation that is pulled out from the freeze. That is needed to guarantee that the result isn't suddenly more poisonnous (the contraints on the result given by the flags might not hold if the input is poison, so they need to be dropped).
The same thing applies to assertSext/assertZext nodes (at least as we say that they may create poison). For a poisoned input the constraints given by the assertSext/assertZext node isn't fulfilled. So we basically need to "drop" the knowledge given by the assert if pulling assett nodes through freeze, or else the result must be treated as still maybe being poison (making the fold invalid).

This patch is limiting the
freeze(assertzext(x)) -> assertzext(freeze(x))
fold to the situation when x is known not to be undef/poison. That means that we actually can drop the freeze, so the new DAG combine in this patch is actually doing
freeze(assertzext(x)) -> assertzext(x)

For the above to be sound the documentation of AssertSext/AssertZext are updated to highlight that it is considered as immediate UB if the input isn't satisfying the asserted condition (unless it is poison).

@bjope bjope requested review from nikic and RKSimon June 5, 2024 15:48
@llvmbot llvmbot added the llvm:SelectionDAG SelectionDAGISel as well label Jun 5, 2024
@llvmbot
Copy link
Member

llvmbot commented Jun 5, 2024

@llvm/pr-subscribers-llvm-selectiondag

Author: Björn Pettersson (bjope)

Changes

This is a new attempt to allow pushing freeze through AssertSext/AssertZext in some situations.

An earlier attempt tried to solve this by handling AssertSext/AssertZext in canCreateUndefOrPoison. Simply saying that result of the assert nodes only are poison/undef if the input is poison/undef. That patch was reverted in
8505c3b

What happened was that we could end up doing folds such as
freeze(assertzext(x)) -> assertzext(freeze(x))
which isn't sound for operations that may "create" poison, unless dropping "poision generating flags" from the operation that is pulled out from the freeze. That is needed to guarantee that the result isn't suddenly more poisonnous (the contraints on the result given by the flags might not hold if the input is poison, so they need to be dropped).
The same thing applies to assertSext/assertZext nodes (at least as we say that they may create poison). For a poisoned input the constraints given by the assertSext/assertZext node isn't fulfilled. So we basically need to "drop" the knowledge given by the assert if pulling assett nodes through freeze, or else the result must be treated as still maybe being poison (making the fold invalid).

This patch is limiting the
freeze(assertzext(x)) -> assertzext(freeze(x))
fold to the situation when x is known not to be undef/poison. That means that we actually can drop the freeze, so the new DAG combine in this patch is actually doing
freeze(assertzext(x)) -> assertzext(x)

For the above to be sound the documentation of AssertSext/AssertZext are updated to highlight that it is considered as immediate UB if the input isn't satisfying the asserted condition (unless it is poison).


Full diff: https://github.com/llvm/llvm-project/pull/94491.diff

4 Files Affected:

  • (modified) llvm/include/llvm/CodeGen/ISDOpcodes.h (+4-1)
  • (modified) llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp (+14)
  • (modified) llvm/test/CodeGen/RISCV/float-convert.ll (+9-11)
  • (modified) llvm/test/CodeGen/RISCV/rv64zbb.ll (+1-1)
diff --git a/llvm/include/llvm/CodeGen/ISDOpcodes.h b/llvm/include/llvm/CodeGen/ISDOpcodes.h
index 0f87e062e2da6..af4f39d77894f 100644
--- a/llvm/include/llvm/CodeGen/ISDOpcodes.h
+++ b/llvm/include/llvm/CodeGen/ISDOpcodes.h
@@ -57,7 +57,10 @@ enum NodeType {
   /// been extended, and the second is a value type node indicating the width
   /// of the extension.
   /// NOTE: In case of the source value (or any vector element value) is
-  /// poisoned the assertion will not be true for that value.
+  /// poisoned the assertion will not be true for that value and the
+  /// corresponding result value will be poison. If a source value isn't
+  /// satisfying the condition being asserted (while not being poison), then
+  /// this is considered as immediate undefined behavior.
   AssertSext,
   AssertZext,
 
diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index 5148b7258257f..f0e65251b80f1 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -15592,6 +15592,20 @@ SDValue DAGCombiner::visitFREEZE(SDNode *N) {
   if (DAG.isGuaranteedNotToBeUndefOrPoison(N0, /*PoisonOnly*/ false))
     return N0;
 
+  // AssertSext/AssertZext nodes are a bit special and need custom
+  // hendling. They are used in contexts when the result is poison only if the
+  // input is poison, but they are still modelled as "canCreateUndefOrPoison" as
+  // the value range for the result is constrained by assumptions that wouldn't
+  // hold if the input is poison (and we can't drop poison-generating flags for
+  // AssertSext/AssertZext in the regular fold below).
+  if (N0.getOpcode() == ISD::AssertSext || N0.getOpcode() == ISD::AssertZext) {
+    // Fold freeze(assertext(x, ...)) -> assertext(x, ...),
+    // iff x is known not to be poison/undef.
+    if (DAG.isGuaranteedNotToBeUndefOrPoison(N0.getOperand(0),
+                                             /*PoisonOnly*/ false))
+      return N0;
+  }
+
   // We currently avoid folding freeze over SRA/SRL, due to the problems seen
   // with (freeze (assert ext)) blocking simplifications of SRA/SRL. See for
   // example https://reviews.llvm.org/D136529#4120959.
diff --git a/llvm/test/CodeGen/RISCV/float-convert.ll b/llvm/test/CodeGen/RISCV/float-convert.ll
index 7eabd3f5f2273..7ce78a8d2529f 100644
--- a/llvm/test/CodeGen/RISCV/float-convert.ll
+++ b/llvm/test/CodeGen/RISCV/float-convert.ll
@@ -966,26 +966,24 @@ define i64 @fcvt_lu_s_sat(float %a) nounwind {
 ; RV64I-NEXT:    sd ra, 24(sp) # 8-byte Folded Spill
 ; RV64I-NEXT:    sd s0, 16(sp) # 8-byte Folded Spill
 ; RV64I-NEXT:    sd s1, 8(sp) # 8-byte Folded Spill
-; RV64I-NEXT:    sd s2, 0(sp) # 8-byte Folded Spill
 ; RV64I-NEXT:    mv s0, a0
-; RV64I-NEXT:    lui a1, 391168
-; RV64I-NEXT:    addiw a1, a1, -1
-; RV64I-NEXT:    call __gtsf2
-; RV64I-NEXT:    sgtz a0, a0
-; RV64I-NEXT:    neg s1, a0
-; RV64I-NEXT:    mv a0, s0
 ; RV64I-NEXT:    li a1, 0
 ; RV64I-NEXT:    call __gesf2
 ; RV64I-NEXT:    slti a0, a0, 0
-; RV64I-NEXT:    addi s2, a0, -1
+; RV64I-NEXT:    addi s1, a0, -1
 ; RV64I-NEXT:    mv a0, s0
 ; RV64I-NEXT:    call __fixunssfdi
-; RV64I-NEXT:    and a0, s2, a0
-; RV64I-NEXT:    or a0, s1, a0
+; RV64I-NEXT:    and s1, s1, a0
+; RV64I-NEXT:    lui a1, 391168
+; RV64I-NEXT:    addiw a1, a1, -1
+; RV64I-NEXT:    mv a0, s0
+; RV64I-NEXT:    call __gtsf2
+; RV64I-NEXT:    sgtz a0, a0
+; RV64I-NEXT:    neg a0, a0
+; RV64I-NEXT:    or a0, a0, s1
 ; RV64I-NEXT:    ld ra, 24(sp) # 8-byte Folded Reload
 ; RV64I-NEXT:    ld s0, 16(sp) # 8-byte Folded Reload
 ; RV64I-NEXT:    ld s1, 8(sp) # 8-byte Folded Reload
-; RV64I-NEXT:    ld s2, 0(sp) # 8-byte Folded Reload
 ; RV64I-NEXT:    addi sp, sp, 32
 ; RV64I-NEXT:    ret
 start:
diff --git a/llvm/test/CodeGen/RISCV/rv64zbb.ll b/llvm/test/CodeGen/RISCV/rv64zbb.ll
index 4d5ef5db86057..dfbefbe27efc1 100644
--- a/llvm/test/CodeGen/RISCV/rv64zbb.ll
+++ b/llvm/test/CodeGen/RISCV/rv64zbb.ll
@@ -1336,7 +1336,7 @@ define i32 @abs_i32(i32 %x) {
 define signext i32 @abs_i32_sext(i32 signext %x) {
 ; RV64I-LABEL: abs_i32_sext:
 ; RV64I:       # %bb.0:
-; RV64I-NEXT:    sraiw a1, a0, 31
+; RV64I-NEXT:    srai a1, a0, 31
 ; RV64I-NEXT:    xor a0, a0, a1
 ; RV64I-NEXT:    subw a0, a0, a1
 ; RV64I-NEXT:    ret

@bjope
Copy link
Collaborator Author

bjope commented Jun 5, 2024

I kind of hope this makes sense.

(Preliminary result is that it avoids regressions in test/CodeGen/VE/Scalar/max.ll and test/CodeGen/AArch64/fast-isel-select.ll, when looking adding it on top of #84924 . So maybe not ground breaking when it comes to reducing amount of test case diffs for that patch.)

@tschuett
Copy link

tschuett commented Jun 5, 2024

Could you move the special handling of assertext(x) into DAG.isGuaranteedNotToBeUndefOrPoison? Then the combine above would handle your case transparently and we can use isGuaranteedNotToBeUndefOrPoisonin other places.

bjope added 2 commits June 5, 2024 21:02
This is a new attempt to allow pushing freeze through
AssertSext/AssertZext in some situations.

An earlier attempt tried to solve this by handling
AssertSext/AssertZext in canCreateUndefOrPoison. Simply saying that
result of the assert nodes only are poison/undef if the input is
poison/undef. That patch was reverted in
  llvm@8505c3b

What happened was that we could end up doing folds such as
  freeze(assertzext(x)) -> assertzext(freeze(x))
which isn't sound for operations that may "create" poison, unless
dropping "poision generating flags" from the operation that is pulled
out from the freeze. That is needed to guarantee that the result
isn't suddenly more poisonnous (the contraints on the result given by
the flags might not hold if the input is poison, so they need to be
dropped).
The same thing applies to assertSext/assertZext nodes (at least as
we say that they may create poison). For a poisoned input the
constraints given by the assertSext/assertZext node isn't fulfilled.
So we basically need to "drop" the knowledge given by the assert if
pulling assert nodes through freeze, or else the result must be
treated as still maybe being poison (making the fold invalid).

This patch is limiting the
  freeze(assertzext(x)) -> assertzext(freeze(x))
fold to the situation when x is known not to be undef/poison. That
means that we actually can drop the freeze, so the new DAG combine
in this patch is actually doing
  freeze(assertzext(x)) -> assertzext(x)

For the above to be sound the documentation of AssertSext/AssertZext
are updated to highlight that it is considered as immediate UB if
the input isn't satisfying the asserted condition (unless it is
poison).
@bjope bjope force-pushed the freeze_assertext branch from a082115 to fc07830 Compare June 5, 2024 19:03
/// poisoned the assertion will not be true for that value and the
/// corresponding result value will be poison. If a source value isn't
/// satisfying the condition being asserted (while not being poison), then
/// this is considered as immediate undefined behavior.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to say that violating the assertion triggers IUB we also need to audit existing uses to comply with that. One obvious case that doesn't is fptoi legalization:

// Assert that the converted value fits in the original type. If it doesn't

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that the fptoi legalization fulfilled this patch. That either the fptoi result is poison (and then it is still poison after the assert), or the result of fptoi would satisfy the assert. This patch is at least not breaking the regression tests related to #66603 .

But maybe this whole idea isn't working anyway. Not sure how to protect from further simplifications of the code leading up to the assert, that could make it less poisonous. It would for example not be legal to just freeze the FP_TO_SINT introduced by legalization, without making sure that the frozen value fulfil the later assert.
So just defining it as immediate UB is probably a lot more complicated than this.

Now I'm just back into the mega-confused-state. And I can't really see how we possibly can handle the (freeze(assertext)) kind of regressions seen in #84924 . Maybe if we try to move the freeze in the other direction when there is an assert (or at least avoid moving it as aggressively in that direction when it might block optimizations.

@efriedma-quic
Copy link
Collaborator

What's the advantage of "if the input is poison, the result is poison; otherwise, if the input isn't zero-extended the behavior is undefined", vs. the much simpler "if the input isn't zero-extended, the result is poison".

@efriedma-quic
Copy link
Collaborator

Actually, thinking about it a bit more, special-casing poison inputs simply doesn't work. We lower poison-producing operations to non-poison-producing operations, so you can't actually prove whether an operation originally produced poison.

Maybe we just want to transform freeze(assertzext(x)) -> freeze(x)?

Copy link
Contributor

@arsenm arsenm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make Assert* have an embedded freeze? If you're doing anything with the

@@ -57,7 +57,10 @@ enum NodeType {
/// been extended, and the second is a value type node indicating the width
/// of the extension.
/// NOTE: In case of the source value (or any vector element value) is
/// poisoned the assertion will not be true for that value.
/// poisoned the assertion will not be true for that value and the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you update the globalisel equivalent documentation to match whatever ends up here for G_ASSERT_SEXT/G_ASSERT_ZEXT?

@bjope
Copy link
Collaborator Author

bjope commented Jun 6, 2024

Actually, thinking about it a bit more, special-casing poison inputs simply doesn't work. We lower poison-producing operations to non-poison-producing operations, so you can't actually prove whether an operation originally produced poison.

Yes. That is a problem. And the idea with classifying it as immediate UB when violating the assert would be that such a lowering would be illegal (when the value is used by an "assertext").

Maybe we just want to transform freeze(assertzext(x)) -> freeze(x)?

I've ended up looking at this since I'm being blocked from pushing the miscompile bugfix in #84924 due to freeze(assertzext(x)) resulting in lit test regressions (although I do not know if it result in any major real world regressions).
The goal here is therefore to keep the property of assertzext even if the result of the assert should be frozen. This to avoid blocking optimizations that could trigger given by the asserted condition.
Neither freeze nor assertzext actually modifies the register, so I don't really see how to freeze the value within a specific value range without adding any real operations. But if we can't eliminate the freeze here (or push it through the assertext), then I guess we should avoid pushing the freeze all the way to the assert in the first place, at least not in situations when it blocks other simplifications.

Anyway, I just want to fix the miscompile issues. And now I've been stuck in the posion/freeze jungle for a couple of months (sometimes just feeling that I get more and more confused the more I look at it).

@bjope
Copy link
Collaborator Author

bjope commented Jun 6, 2024

Can we make Assert* have an embedded freeze? If you're doing anything with the

Something along my earlier ideas here #84924 (comment) ?
I.e. adding some bool parameter to Assertext to indicate if it propagates poision or not. (ore one could see it as the bool controls if the Assertext also is a freeze).
I did try something simpler in this patch. But now I'm starting to think that it was a bad short-cut to skip that extra bool parameter. If adding such a paramter we can narrow the impact here to for example Assert*ext nodes introduced by call/argument lowering.

@nikic
Copy link
Contributor

nikic commented Jun 6, 2024

@efriedma-quic The context here is basically that we want things like zeroext i1 to still provide useful information even after a freeze. This is sound if we say that zeroext/signext violations are IUB, and I don't think there are any concerns with saying that. However, the SDAG representation via AssertZext/AssertSext is currently poison-generating, so we lose the information when there is a freeze. And we have some uses of AssertZext that are not compatible with IUB semantics.

So I think the choices here are either:

  1. Say that AssertZext violation is IUB, and remove uses of it in cases where poison semantics are necessary. (I agree that the special poison-in-poison-out handling in this PR doesn't really work.)
  2. Provide a flag on AssertZext that determines whether it is IUB, or provide two separate nodes for both behaviors.

@efriedma-quic
Copy link
Collaborator

So we need instant-UB specifically for calling-convention stuff? I guess that makes sense. Do we actually freeze operands to calls to ensure the bits are set correctly, though?

@nikic
Copy link
Contributor

nikic commented Jun 7, 2024

So we need instant-UB specifically for calling-convention stuff? I guess that makes sense. Do we actually freeze operands to calls to ensure the bits are set correctly, though?

We don't, but I didn't think this would be necessary. Do you have a problematic example in mind?

@efriedma-quic
Copy link
Collaborator

Something like the following:

declare void @g(i1 zeroext)
define void @f(float %f) {
  %i = fptoui float %f to i1
  call void @g(i1 zeroext %i)
  ret void
}

Targeting AArch64, we don't actually zero-extend the argument. Which is a problem if it's instant UB.

@efriedma-quic
Copy link
Collaborator

That said, it's only really a problem for function arguments that aren't noundef; maybe we can just skip emitting AssertZExt for such arguments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
llvm:SelectionDAG SelectionDAGISel as well
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants