-
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
recursive-patterns(11): use stack spiller for switch expressions #25697
recursive-patterns(11): use stack spiller for switch expressions #25697
Conversation
This refactoring moves the spiller between initial lowering and lambda lowering
There are two iterations:
I separated them to make review easier, as the rename and move makes it look like a file was deleted and one added. This is built on top of other pending reviews, so even though there are only two new iteration here, you will see iterations from previous PRs. Only the last two matter for this PR. @agocke @cston Can you review this please? |
- Added some improved debug-time display for synthesized locals - Added some invariant checking regarding bound nodes. - Move invocation of the spiller into LocalRewriter.Rewrite - Add an error for when an expression tree contains a switch expression - Made the local substituter apply at the top level in the expression spiller because expressions can now contain statements (BoundSpillSequence) - Move the error ERR_ByRefTypeAndAwait to IteratorAndAsyncCaptureWalker - Skip broken tests due to dotnet#25702
@dotnet/roslyn-interactive I had to skip about 50 interactive tests that are producing bound trees that violate bound tree invariants (they use locals that are not declared). Please see #25702 for details. |
Alternatively, perhaps change In reply to: 376626943 [](ancestors = 376626943) Refers to: src/Compilers/Core/Portable/Collections/IOrderedReadOnlySet.cs:1 in f7b666d. [](commit_id = f7b666d, deletion_comment = False) |
// There is at least one kind of anlysis that cares about this distinction - async stack spilling | ||
// Used internally to track `In` arguments that were specified with `In` modifier | ||
// as opposed to those that were specified with no modifiers and matched `In` parameter. | ||
// There is at least one kind of anlysis that cares about this distinction - hoisting |
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.
anlysis [](start = 41, length = 7)
Typo. #Resolved
@@ -5348,4 +5348,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ | |||
<data name="WRN_SwitchExpressionNotExhaustive_Title" xml:space="preserve"> | |||
<value>The switch expression does not handle all possible inputs (it is not exhaustive).</value> | |||
</data> | |||
<data name="ERR_ExpressionTreeContainsSwitchExpression" xml:space="preserve"> | |||
<value>An expression tree may not contain a switch-expression.</value> |
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.
switch-expression [](start = 48, length = 17)
Perhaps switch expression
(no hyphen) to match the other diagnostics. #Resolved
@@ -172,5 +194,227 @@ internal string GetDebuggerDisplay() | |||
} | |||
return result; | |||
} | |||
|
|||
public void CheckLocalsDefined() |
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.
CheckLocalsDefined [](start = 20, length = 18)
[Conditional("DEBUG")]
#Resolved
localsScanner.Free(); | ||
} | ||
|
||
void AddAll(ImmutableArray<LocalSymbol> locals) |
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.
void [](start = 12, length = 4)
private
for these three methods. #Resolved
{ | ||
if (_inExpressionLambda) | ||
{ | ||
Error(ErrorCode.ERR_ExpressionTreeContainsSwitchExpression, node); |
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.
ERR_ExpressionTreeContainsSwitchExpression [](start = 32, length = 42)
Is there a test for this code path? #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.
builder.AddLocals(node.Locals); | ||
var value = VisitExpression(ref builder, node.Value); | ||
value = Spill(builder, value); | ||
return builder.Update(value); | ||
} | ||
|
||
public override BoundNode VisitAddressOfOperator(BoundAddressOfOperator node) |
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.
ic override BoundNode Vi [](start = 12, length = 24)
Why is this necessary? Consider adding a comment. #Resolved
public override BoundNode VisitLocalFunctionStatement(BoundLocalFunctionStatement node) | ||
{ | ||
var oldCurrentMethod = _F.CurrentMethod; | ||
_F.CurrentMethod = node.Symbol; |
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.
node.Symbo [](start = 30, length = 11)
We don't need a separate SpillSequenceSpiller
instance when visiting lamba and local function bodies? #ByDesign
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.
Correct, because things are spilled up to the statement level. Lambdas and local functions have a top-level block containing statements at this stage.
In reply to: 177557467 [](ancestors = 177557467)
{ | ||
// CS4013: Instance of type '{0}' cannot be used inside an anonymous function, query expression, iterator block or async method | ||
diagnostics.Add(ErrorCode.ERR_SpecialByRefInLambda, syntax.Location, type); | ||
diagnostics.Add(ErrorCode.ERR_ByRefTypeAndAwait, local.Locations[0], local.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.
ERR_ByRefTypeAndAwait [](start = 50, length = 21)
Does this correspond to the restricted type case previously in AwaitExpressionSpiller.AddLocal
? If so, perhaps add Debug.Assert(local.Type.IsRestrictedType());
. #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.
That is true by construction. There is only one line that adds to the set from which the symbol was fetched:
if (type.IsRestrictedType())
{
// ...
_lazyDisallowedCaptures.Add(variable, syntax);
}
In reply to: 177570217 [](ancestors = 177570217)
case SyntaxKind.SwitchExpression: | ||
case SyntaxKind.AwaitExpression: | ||
// These translate into a BoundSpillSequence, which is then translated into a block | ||
// containins temps required for spilling subexpressions. That block has the syntax of the 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.
containins [](start = 23, length = 10)
Typo #Resolved
// The resulting nodes will be "spilled" to move such statements to the top | ||
// level (i.e. into the enclosing statement list). | ||
_needsSpilling = true; | ||
BoundLocal replacement = _factory.StoreToTemp( |
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 it possible that this needs to be a ref temp? #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.
await
is never an lvalue, so this temp is never a ref temp.
In general case we can have byref spills.
In a case of await
, they will be later decomposed into something that can be hoisted - like {array, index} pairs or will produce an error if it is a true byref spill - like a result of byref method call.
In a case when spill is caused by expression switch or stackalloc (in the future), byref spills would work just fine since we do not need to hoist - ref temps stay ref temps.
Perhaps need a bit more care to make sure that "readonliness" matches. - I.E. if spilling an in
argument or a receiver of readonly
struct member, the temp must be ref readonly
. Not sure if this is already handled correctly.
It may be particularly important when spilling receivers of struct/generic calls, since mismatch in "readonly" may cause different behavior for the copying of the receiver. #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.
The part about "readonly" is probably better handled as a separate workitem. It would be a new scenario for spiller.
In await
case we decompose ref spills, so readonlyness of temps did not matter since they would not survive. #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.
LGTM
{ | ||
switch (dag) | ||
{ | ||
case BoundEvaluationPoint p: | ||
case BoundEvaluationDecisionDagNode p: |
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.
Are these types sealed? (for faster typechecks) #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.
The bound node generator only generates abstract
or sealed
types.
In reply to: 178199580 [](ancestors = 178199580)
if (localRewriter._needsSpilling && !loweredStatement.HasErrors) | ||
{ | ||
// Move spill sequences to a top-level statement. This handles "lifting" await and the switch expression. | ||
// PROTOTYPE(patterns2): should this be something that the caller does? It isn't really a "local" rewrite. |
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 it is better be handled by the caller, similarly to handling await
in exception handling regions.
- to have clear sequence of tree transformation.
- to have better attribution when investigating perf impact of lowering passes. #ByDesign
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 placed this here because there is more than one caller. However We could duplicate this code for each caller. I am not sure which is better, but this simplifies the job of the caller.
In reply to: 178200828 [](ancestors = 178200828)
@@ -14,14 +14,14 @@ | |||
|
|||
namespace Microsoft.CodeAnalysis.CSharp | |||
{ | |||
internal sealed class AwaitExpressionSpiller : BoundTreeRewriterWithStackGuard | |||
internal sealed class SpillSequenceSpiller : BoundTreeRewriterWithStackGuard |
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.
We are likely unable to handle some expressions that are not permitted in async methods - unsafe stuff and such.
I think it is a separate workitem, but we need to make sure that all or nearly all bound expresions can be spilled. #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.
The switch expression can appear in an unsafe method, so spilling needs to work with almost everything. If you have specific scenarios that would illustrate the issues, that would be very helpful. Please file as a fresh issue.
In reply to: 178203355 [](ancestors = 178203355)
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
I had some comments, but I think it may be better to handle them as a separate change
DecisionPoint, | ||
WhenClause, | ||
Decision, | ||
DecisionDag, |
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.
Does the order of these members matters (in a sense that related nodes would be closer together for better switch optimizations)? Currently decision tree nodes come in the middle of statements. #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.
There are only four kinds of dag nodes, and they are rarely treated the same. I will file an issue to move them all after statements.
In reply to: 178205735 [](ancestors = 178205735)
This refactoring moves the spiller between initial lowering and lambda lowering
There are two iterations:
I separated them to make review easier, as the rename and move makes it look like a file was deleted and one added.
This is built on top of other pending reviews, so even though there are only two new iteration here, you will see iterations from previous PRs. Only the last two matter for this PR.
@agocke @cston Can you review this please?
/cc @dotnet/roslyn-compiler