-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Fix long tuples in PatternLocalRewriter #47893
Conversation
@@ -488,7 +488,7 @@ bool usesOriginalInput(BoundDecisionDagNode node) | |||
bool canShareInputs, | |||
out BoundExpression savedInputExpression) | |||
{ | |||
int count = loweredInput.Arguments.Length; | |||
int count = loweredInput.Type.TupleElements.Length; |
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.
For tuple containing > 7 elements (10 elements for example), the previous value of count will be 8 (the TRest position) instead of 10, causing the loop after a few lines to run a fewer iteration than needed.
@@ -497,7 +497,7 @@ bool usesOriginalInput(BoundDecisionDagNode node) | |||
{ | |||
var field = loweredInput.Type.TupleElements[i].CorrespondingTupleField; | |||
Debug.Assert(field != null); | |||
var expr = loweredInput.Arguments[i]; | |||
var expr = getExpression(i, loweredInput); |
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.
Here, we can't just access Arguments[i] for two reasons:
- If i == 8, the result expression will be a tuple, this is not intended.
- If i > 8, this will throw an exception due to the other fix I made.
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.PatternLocalRewriter.cs
Outdated
Show resolved
Hide resolved
@gafter if you are available, we'd appreciate a review 😄 |
{ | ||
public static void Main() | ||
{ | ||
int x1 = (1, 1, 1, 1, 1, 1, 1, 1, 1, 1) switch |
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.
Please split out this test and verify the IL. I don't think we need to verify IL on all of these variations, but it would be good to have 1 of them verified.
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.
@333fred I feel it's safer to verify the IL for different cases because in the past, the compiler had different behavior between the 8 elements case, and the > 8 elements case.
I'd at least verify the IL for 2 cases:
- 8 elements
- Greater than 8 elements
Done review pass (commit 3). |
Also, it looks like another test is failing with this change. |
I'll give it a look and try to fix that with the test changes you suggested. |
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.PatternLocalRewriter.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.PatternLocalRewriter.cs
Show resolved
Hide resolved
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.
Done with review pass (iteration 3). Minor comments only
With the change I introduced, Another problem is that I'm unsure of "who" calls GetArgumentRefKind. Is it used for things other than tuples (i.e. normal method parameters) in which case things may get a bit more complex (opened #47911 in a hope that test failures will answer). Also I spotted an assertion that is now passing just "by luck": private void EmitArguments(ImmutableArray<BoundExpression> arguments, ImmutableArray<ParameterSymbol> parameters, ImmutableArray<RefKind> argRefKindsOpt)
{
// We might have an extra argument for the __arglist() of a varargs method.
Debug.Assert(arguments.Length == parameters.Length || arguments.Length == parameters.Length + 1, "argument count must match parameter count");
Debug.Assert(parameters.All(p => p.RefKind == RefKind.None) || !argRefKindsOpt.IsDefault, "there are nontrivial parameters, so we must have argRefKinds");
Debug.Assert(argRefKindsOpt.IsDefault || argRefKindsOpt.Length == arguments.Length, "if we have argRefKinds, we should have one for each argument");
for (int i = 0; i < arguments.Length; i++)
{
RefKind argRefKind = GetArgumentRefKind(arguments, parameters, argRefKindsOpt, i);
EmitArgument(arguments[i], argRefKind);
}
} This assertion: // We might have an extra argument for the __arglist() of a varargs method.
Debug.Assert(arguments.Length == parameters.Length || arguments.Length == parameters.Length + 1, "argument count must match parameter count"); Assumes a max difference of 1 between arguments length and parameters length. Because the corresponding test uses a tuple of 9 elements, and arguments is 8 (where the 8th argument is a tuple itself holding the 8th and 9th values). the assertion passes. But if it's a tuple of 10 elements. arguments.Length would still be 8 and the assertion will fail. Given that, I'm now less confident with the change I made. It breaks some existing assumptions. I could just remove the assertion in |
Consider doing this as a static method on |
The existing code needs to get a I found a class named For the failing assertions, I think I may surround them with an if condition so they are executed for non-tuple parameters only. |
// We might have an extra argument for the __arglist() of a varargs method.
Debug.Assert(arguments.Length == parameters.Length || arguments.Length == parameters.Length + 1, "argument count must match parameter count"); Perhaps the right thing to do is break this assert out into a conditional method that can verify the right thing. It would verify that:
You'll have to surround such a method if |
Actually I think I was incorrect when I said:
This isn't correct. ValueTuple constructor have at most 8 arguments. Even if the elements are greater than that, it doesn't make sense to call ArgumentRefKind with index > 7. So The callers & caller's assertion needs some tweaks. Or even I think the original fix shouldn't cascade like this. |
GitHub keeps presenting my name at the top of the completion list, then the "remove all assignees" button pops in.. |
One thing to notice, when this was working in past, the generated code was using pointers in an unsafe context. The change in this PR tries to make the code works again, but in non-unsafe fashion. But I can't get it to work correctly. @RikkiGibson @jcouv Are you aware of any PRs in the past that was around making the generated code non-unsafe? |
@Youssef1313 I messaged you on gitter, let's figure out the answer to that and how to move forward. |
Summary of the situation: Pattern matching of tuples of more than 8 elements (in this scenario at least) has not worked for some time. At least not since the 16.5 time frame. For a while, pattern matching tuples with exactly 8 elements has worked, but later the semantics of the emitted code were broken. Perhaps we should just look at the up-to-date implementation and try to fix it to work with indefinitely many elements, perhaps using a "too many locals" or similar diagnostic if we hit a meaningful limit. |
} | ||
|
||
[Fact, WorkItem(47878, "https://github.com/dotnet/roslyn/issues/47878")] | ||
public void VerifyIL_8ElementsTuple() |
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 the IL for this test and the next test correct?
@RikkiGibson Looks like it should be working now. But my changes removed a whole array that used to exist, which makes me a little bit less confident. Can this change break some scenario that is currently not tested? |
} | ||
|
||
var rewrittenDag = decisionDag.Rewrite(makeReplacement); | ||
savedInputExpression = loweredInput.Update( | ||
loweredInput.Constructor, arguments: newArguments.ToImmutableAndFree(), loweredInput.ArgumentNamesOpt, loweredInput.ArgumentRefKindsOpt, | ||
loweredInput.Constructor, arguments: loweredInput.Arguments, loweredInput.ArgumentNamesOpt, loweredInput.ArgumentRefKindsOpt, |
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'm confused by the change to get rid of the newArguments
array. I thought that would have undesired effects, particularly related to reuse of user temps or side effects.
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.
@RikkiGibson I was expecting to see some test failures related to this too, but I don't know what scenarios exactly that should fail. Do you know any scenarios that should fail to add tests for them?
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 tried the following scenario to induce a failure but it continued to have the expected side-effects in your branch:
[Fact, WorkItem(47878, "https://github.com/dotnet/roslyn/issues/47878")]
public void VerifyIL_8ElementsTuple_SideEffects()
{
var text = @"
using System;
public class C
{
public static void Main()
{
int x = (M(1), M(2), M(3), M(4), M(5), M(6), M(7), M(8)) switch
{
(1, 2, 3, 4, 5, 6, 7, 7) => 1,
(1, 2, 3, 4, 5, 6, 7, 8) => 1,
_ => -1,
};
Console.WriteLine(x);
}
static int M(int x)
{
Console.Write(x);
return x;
}
}
";
CompileAndVerify(text, expectedOutput: "123456781");
}
I think the next thing I would try would be to make many of the tuple components local variables and see if the lowering code reuses the local variables properly instead of introducing new ones.
I'll try to debug in more detail soon.
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.
Here's the scenario where we have unexpected side effects:
[Fact, WorkItem(47878, "https://github.com/dotnet/roslyn/issues/47878")]
public void VerifyIL_8ElementsTuple_SideEffects_02()
{
var text = @"
using System;
using System.Runtime.CompilerServices;
public class C
{
public static void Main()
{
try
{
int x = (M(1), M(2), M(3), M(4), M(5), M(6), M(7), M(8)) switch
{
(1, 2, 3, 4, 5, 6, 7, 7) => 1,
};
Console.WriteLine(x);
}
catch (SwitchExpressionException ex)
{
Console.Write(""💥"");
}
}
static int M(int x)
{
Console.Write(x);
return x;
}
}
namespace System.Runtime.CompilerServices
{
public class SwitchExpressionException : InvalidOperationException
{
public SwitchExpressionException() {}
public SwitchExpressionException(object unmatchedValue) => UnmatchedValue = unmatchedValue;
public object UnmatchedValue { get; }
}
}
";
CompileAndVerify(text, expectedOutput: "12345678💥");
}
With your change, the actual output is 1234567812345678💥
. Let's revert that part of the change.
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.
@RikkiGibson That will get us to the assertion failure too. I'm still not sure what is the appropriate fix. I'll add this test and give it a try again, but not sure if I would be able to get the correct fix.
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 problem is that when we "unwrap" the tuple elements as is done in this PR, the length of the arguments no longer matches the length of the arguments ot the top-level object creation node. i.e. loweredInput.Arguments.Length
should never be more than 8, and tuples of size 8 or larger must nest their arguments in additional object creation nodes.
I can try and look a bit to see how to do this--the fix isn't coming to mind instantly. But to get the behavior of the scenario fixed in 16.8 we may want to take #48028 and then optimize it later.
I will move this back to draft status. Feel free to mark it ready for review again if you want us to take another look later. |
Closing this stale PR. |
Fixes #47878
To explain the changes:
loweredInput.Arguments can have at max 8 arguments, with the 8th argument being another tuple. This is due to the available
ValueTuple
s in the runtime:I fixed the loop to have the exact number of items (which can be greater than 8), and introduced a static local function which gets the BoundExpression for the corresponding index.
index < 7
, just returnArguments[index]
.index >= 7
, gets the correspondingTRest
tuple, and apply the same logic forindex - 7
.