Skip to content

[TableGen] Make !and and !or short-circuit #113963

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

Merged
merged 3 commits into from
Nov 7, 2024

Conversation

mshockwave
Copy link
Member

@mshockwave mshockwave commented Oct 28, 2024

By preemptively simplifying the result of !and and !or, we can fold some of the conditional operators, like !if or !cond, as early as possible.

By preemptively simplifying the result of `!and`, we can fold some of
the conditional operators, like `!if` or `!cond`, as early as possible.
@llvmbot
Copy link
Member

llvmbot commented Oct 28, 2024

@llvm/pr-subscribers-tablegen

Author: Min-Yih Hsu (mshockwave)

Changes

By preemptively simplifying the result of !and, we can fold some of the conditional operators, like !if or !cond, as early as possible.


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

2 Files Affected:

  • (modified) llvm/lib/TableGen/Record.cpp (+17)
  • (modified) llvm/test/TableGen/true-false.td (+14)
diff --git a/llvm/lib/TableGen/Record.cpp b/llvm/lib/TableGen/Record.cpp
index 1d71482b020b22..1f01b82685718a 100644
--- a/llvm/lib/TableGen/Record.cpp
+++ b/llvm/lib/TableGen/Record.cpp
@@ -1543,6 +1543,23 @@ const Init *BinOpInit::resolveReferences(Resolver &R) const {
   const Init *lhs = LHS->resolveReferences(R);
   const Init *rhs = RHS->resolveReferences(R);
 
+  if (getOpcode() == AND) {
+    // Short-circuit. Regardless whether this is a logical or bitwise
+    // AND.
+    if (lhs != LHS)
+      if (const auto *LHSi = dyn_cast_or_null<IntInit>(
+              lhs->convertInitializerTo(IntRecTy::get(getRecordKeeper())))) {
+        if (!LHSi->getValue())
+          return LHSi;
+      }
+    if (rhs != RHS)
+      if (const auto *RHSi = dyn_cast_or_null<IntInit>(
+              rhs->convertInitializerTo(IntRecTy::get(getRecordKeeper())))) {
+        if (!RHSi->getValue())
+          return RHSi;
+      }
+  }
+
   if (LHS != lhs || RHS != rhs)
     return (BinOpInit::get(getOpcode(), lhs, rhs, getType()))
         ->Fold(R.getCurrentRecord());
diff --git a/llvm/test/TableGen/true-false.td b/llvm/test/TableGen/true-false.td
index 597ad9f5ecc8e7..a9094884dbdb9f 100644
--- a/llvm/test/TableGen/true-false.td
+++ b/llvm/test/TableGen/true-false.td
@@ -67,6 +67,20 @@ def rec7 {
   bits<3> flags = { true, false, true };
 }
 
+// The `!and` should be short-circuit such that `!tail` on empty list will never
+// be evaluated.
+// CHECK: def rec8
+// CHECK:   list<int> newSeq = [];
+// CHECK:   list<int> newSeq2 = [];
+
+class Foo <list<int> seq = []> {
+  bit containsStr = !ne(!find(NAME, "BAR"), -1);
+  list<int> newSeq  = !if(!and(!not(!empty(seq)), containsStr), !tail(seq), seq);
+  list<int> newSeq2 = !if(!and(containsStr, !not(!empty(seq))), !tail(seq), seq);
+}
+
+def rec8 : Foo<>;
+
 #ifdef ERROR1
 // ERROR1: Record name '1' is not a string
 

@@ -1543,6 +1543,23 @@ const Init *BinOpInit::resolveReferences(Resolver &R) const {
const Init *lhs = LHS->resolveReferences(R);
const Init *rhs = RHS->resolveReferences(R);

if (getOpcode() == AND) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need similar short circuit for OR as well?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, I had thought this before. The problem is that since !or is for both logical and bitwise operation, we can't short circuit something like this !or(true, <unknown / unresolved>) until the second operand is resolved (in which case it's not a short circuit anymore) because we're not sure if the second operand is a int (bitwise OR) or a bit (logical OR).

That said, I think a separate logical OR / AND like you suggested might help us to apply short-circuit with more confidence.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, I had thought this before. The problem is that since !or is for both logical and bitwise operation, we can't short circuit something like this !or(true, <unknown / unresolved>) until the second operand is resolved (in which case it's not a short circuit anymore) because we're not sure if the second operand is a int (bitwise OR) or a bit (logical OR).

I just made a blunder and forgot that !or(<all ones>, ...) can also be short-circuited. I'll add this case into this patch as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

The short-circuit for !or against all ones was just added.

@Paul-C-Anagnostopoulos
Copy link

!and and !or are bitwise operators that take an arbitrary number of arguments. Given that they are not documented to short-circuit, isn't this an incompatible change?

(It's been awhile, so my memory may be failing me.)

@jurahul
Copy link
Contributor

jurahul commented Oct 29, 2024 via email

@Paul-C-Anagnostopoulos
Copy link

Yes, it may be that all the operators that return integers or booleans have no side-effects, but that seems like something we should not rely on. I'm tempted to suggest that we add !land and !lor instead, which would take only bit arguments. But that isn't very C++-ish.

I'll leave the final decision to someone with a stronger opinion on the matter.

@mshockwave
Copy link
Member Author

mshockwave commented Oct 29, 2024

all the operators that return integers or booleans have no side-effects, but that seems like something we should not rely on

Is there a case where side effects might be produced in a (bang operator) expression? To my best understandings bang operators are all designed to be very..."Lisp" without any side effect. And even if we intermix other TableGen constructions into the expression, like !and(SomeClass<v>.field, ...) , I can't think of a case where side effects are produced from those constructions.

More generally speaking, given the fact that our current !or and !and are for both bitwise and logical, I agree that short-circuit behavior should not be generally applicable to them. But !and with one of the operands being zero is the case that will always work regardless of being logical or bitwise (assuming we have no side, of course, which I'd commented above). That's why this patch is relatively specific to !and.

we add !land and !lor instead, which would take only bit arguments. But that isn't very C++-ish.

@Paul-C-Anagnostopoulos could you elaborate a little more on this point? I thought C/C++ also have separate logical and bitwise operators.

@mshockwave
Copy link
Member Author

To give a little more background, this is the case (which is also the attached test case) that motivates me to come up with this patch:

class Foo <list<int> seq = []> {
   bit containsStr = !ne(!find(NAME, "BAR"), -1);
   list<int> newSeq  = !if(!and(!not(!empty(seq)), containsStr), !tail(seq), seq);
 }

 def rec8 : Foo<>;

Since the !if condition cannot be resolved immediately, TableGen resolver actually resolve both !tail(seq) and seq right away, and trigger an assertion in the process because !tail doesn't like seq being empty.

There are two ways to solve this problem: (1) Fix !if to postpone the resolution of its true and false values (2) make !and short-circuit.

I post this patch to do (2) first because I thought it can benefit more cases in addition to !if conditions. I'm thinking of posting a patch to do (1) as well and I'm happy to know what's your thought on that.

@Paul-C-Anagnostopoulos
Copy link

Sheesh, of course you are right. C++ has both bitwise and logical operators. See what happens when I don't code any C/C++ for a year? It all goes right out of my brain.

I believe you are correct that no bang operators produce side-effects. So therefore it should be okay to short-circuit !and when a 0 argument is encountered, and !or when an all-1s argument is encountered.

Be sure to document this, or otherwise there is no point in doing it.

Or, in keeping with C++, we could leave !and and !or alone and add !land and !lor specifically for this purpose. They would accept only bit arguments. If we go this route, I'd add !lxor, too, even though it doesn't short-circuit.

@optimisan
Copy link
Contributor

There are two ways to solve this problem: (1) Fix !if to postpone the resolution of its true and false values (2) make !and short-circuit.

I post this patch to do (2) first because I thought it can benefit more cases in addition to !if conditions. I'm thinking of posting a patch to do (1) as well and I'm happy to know what's your thought on that.

(1) would be very helpful imo
I was gonna take up (1) as well; I have a sort of workaround here because !if crashes here #107829 (comment) although this patch unifies the two-step resolution.

@mshockwave
Copy link
Member Author

I have a sort of workaround here because !if crashes here #107829 (comment) although this patch unifies the two-step resolution.

Thanks for pointing out. I was actually thinking of a simple fix which removes the immediate resolutions of both operand in !if, but it seems like you have tried it and observed stack overflow (infinite recursive function calls)?

@optimisan
Copy link
Contributor

I haven't tried removing the immediate resolution yet, but unifying the resolution works around this bug in !if - but only for the case of instantiating anonymous records (not for def with subclasses).
I think fixing !if should be the right way to go next.

@mshockwave
Copy link
Member Author

I believe you are correct that no bang operators produce side-effects. So therefore it should be okay to short-circuit !and when a 0 argument is encountered, and !or when an all-1s argument is encountered.

Be sure to document this, or otherwise there is no point in doing it.

I just added short-circuit for !or and the corresponding documentations

@mshockwave mshockwave changed the title [TableGen] Make !and short-circuit when either of the operand is zero [TableGen] Make !and and !or short-circuit Oct 30, 2024
@mshockwave mshockwave requested a review from jurahul October 31, 2024 17:23
Copy link
Contributor

@optimisan optimisan left a comment

Choose a reason for hiding this comment

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

LGTM atleast;
maybe others will review?

Comment on lines 78 to 84
class Foo <list<int> seq = []> {
bit containsStr = !ne(!find(NAME, "BAR"), -1);
list<int> newSeq = !if(!and(!not(!empty(seq)), containsStr), !tail(seq), seq);
list<int> newSeq2 = !if(!and(containsStr, !not(!empty(seq))), !tail(seq), seq);
list<int> newSeq3 = !if(!or(containsStr, -1), seq, !tail(seq));
list<int> newSeq4 = !if(!or(-1, containsStr), seq, !tail(seq));
}
Copy link
Contributor

@optimisan optimisan Nov 1, 2024

Choose a reason for hiding this comment

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

Is it better to have a simpler test since it's supposed to only check if !and/!or is shorting properly?
Something like this perhaps:

class Foo <int zero = 0, list<int> seq = []> {
    int unresolved = zero;
    list<int> andLHS = !if(!and(zero, unresolved), !tail(seq), seq);
    ...
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

if (Opc == AND || Opc == OR) {
// Short-circuit. Regardless whether this is a logical or bitwise
// AND/OR.
// Ideally we could also short-circuit `!or(true, ...)`, but it's
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment about !or now seems stale.

Copy link
Member Author

Choose a reason for hiding this comment

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

The comment about !or now seems stale.

It is up-to-date: in the case of !or, TableGen first casts every operands into int, which is a 64 integer underlying. A true is casted to 1 (rather than all ones) and because we only support short-circuit against all ones -- namely -1 -- a true is actually not short circuited. The comment that follows also clarifies why we can't short circuit against 1.

(Opc == OR && LHSi->getValue() == -1))
return LHSi;
}
if (const auto *RHSi = dyn_cast_or_null<IntInit>(
Copy link
Contributor

Choose a reason for hiding this comment

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

In C/C++, the short-circuit is only applied to the LHS of the operator. That is, if the RHS happens to be 0 for an &&, the LHS is still evaluated. This seems to diverge from that, in the sense if RHS is 0, the LHS won't be evaluated. Is that the intent? Or should we abide by C/C++ style short-circuit to reduce the element of surprise?

Copy link
Contributor

Choose a reason for hiding this comment

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

Or is this still under the umbrella that these evaluations do not produce side effects? I'd think even then, there may be value in just sticking with well known rules. I am assuming this is also sufficient for the cases you'd like to handle.

Copy link
Member Author

Choose a reason for hiding this comment

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

You have a good point here. My intention was that in the case of something like !and(<unresolved>, 0) we can avoid resolving the first operand, because it's futile.

may be value in just sticking with well known rules. I am assuming this is also sufficient for the cases you'd like to handle

Yeah I agree, I'll update the PR accordingly.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's done now.

@jurahul
Copy link
Contributor

jurahul commented Nov 5, 2024

Change LGTM to me, so approving, but would like another approval from someone else as well.

@mshockwave mshockwave merged commit e8b70e9 into llvm:main Nov 7, 2024
9 checks passed
@mshockwave mshockwave deleted the patch/tblgen-and-short-circuit branch November 7, 2024 18:22
Groverkss pushed a commit to iree-org/llvm-project that referenced this pull request Nov 15, 2024
The idea is that by preemptively simplifying the result of `!and` and `!or`, we can fold
some of the conditional operators, like `!if` or `!cond`, as early as
possible.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants