Skip to content

Conversation

@jcouv
Copy link
Member

@jcouv jcouv commented Nov 29, 2025

This PR generalizes the LocalRewritingValidator enforcement and allows declaring how far bound nodes are supposed to survive in the lowering pipeline.
Addresses part of #16057

@jcouv jcouv self-assigned this Nov 29, 2025
@CyrusNajmabadi
Copy link
Member

Cool idea!

@jcouv jcouv force-pushed the assert-lowering branch 5 times, most recently from 142ac09 to 70574bb Compare December 1, 2025 16:40
@jcouv jcouv marked this pull request as ready for review December 1, 2025 22:09
@jcouv jcouv requested a review from a team as a code owner December 1, 2025 22:09
LocalRewriting,
ClosureConversion,
StateMachineRewriting,
None
Copy link
Member

@jjonescz jjonescz Dec 2, 2025

Choose a reason for hiding this comment

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

Perhaps this should be called "All" instead of "None"? Since "does not survive none" sounds wrong. #Resolved

<!--
This node will not survive the local rewriting,
except in some scenarios in a Linq Expression Tree when the containing node
survives.
Copy link
Member

@jjonescz jjonescz Dec 2, 2025

Choose a reason for hiding this comment

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

except in some scenarios in a Linq Expression Tree

Are these cases tested? Why doesn't the validator fail for them? Are the bound nodes flagged as erroneous? #WontFix

Copy link
Member Author

Choose a reason for hiding this comment

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

I was not able to hit that, so I don't think the comment was correct. I'll revisit if we do end up hitting the assertion

}

/// <summary>
/// Note: do not use a static/singleton instance of this type, as it holds state.
Copy link
Member

@jjonescz jjonescz Dec 2, 2025

Choose a reason for hiding this comment

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

nit: this comment looks unnecessary since the class has a private constructor #Resolved

@@ -6,6 +6,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
Copy link
Member

@jjonescz jjonescz Dec 2, 2025

Choose a reason for hiding this comment

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

nit: this added using looks unnecessary #Resolved

public static void AssertAfterInitialBinding(BoundNode node)
{
#if DEBUG
Assert(node, PipelinePhase.InitialBinding);
Copy link
Member

@jjonescz jjonescz Dec 2, 2025

Choose a reason for hiding this comment

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

Consider exposing just the single Assert method (and yes, having the enum outside #if DEBUG) to avoid this boilerplate #ByDesign

Copy link
Member Author

Choose a reason for hiding this comment

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

I had that initially, but that means shipping the enum and adding #if DEBUG at the call-sites.
I prefer this way, where we limit what is in the release assembly and we don't need #if DEBUG at the call-sites (we hide the boilerplate here)

Copy link
Member

Choose a reason for hiding this comment

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

I think the callsites wouldn't need #if DEBUG since the method is marked as [Conditional]

Copy link
Member Author

Choose a reason for hiding this comment

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

Let me try this again, but I think callsites do need #if DEBUG if a PipelinePhase is given as argument and that type is DEBUG-only.

Copy link
Member

@jjonescz jjonescz Dec 3, 2025

Choose a reason for hiding this comment

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

Right, I meant that the enum wouldn't be DEBUG-only.

The current state seems fine to me though, thanks.

}
}

#if DEBUG
Copy link
Member

@jjonescz jjonescz Dec 2, 2025

Choose a reason for hiding this comment

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

Outside DEBUG, consider having a private constructor of PipelinePhaseValidator too (perhaps a one that throws unreachable exception). #Resolved

{
if (node is BoundIfStatement)
{
Fail(node);
Copy link
Member

@jjonescz jjonescz Dec 2, 2025

Choose a reason for hiding this comment

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

Looks like originally we didn't expect BoundIfStatement to survive local rewriting even if it had errors but now the check is relaxed. Perhaps we can preserve that by adding another knob to BoundNodes.xml ("doesn't survive even with errors"). #WontFix

Copy link
Member Author

Choose a reason for hiding this comment

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

That's true. However, I'm fine with this minor relaxation given that we're overall tightening and uniformizing the checks. Seems fine to lose this special handling.

@jcouv jcouv requested review from a team and jjonescz December 3, 2025 15:15
@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 3, 2025

DoesNotSurvive="InitialBinding"?

BTW, it looks like content of this file is out of date. #Closed


Refers to: src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml:305 in b5ae4aa. [](commit_id = b5ae4aa, deletion_comment = False)

InitialBinding,
LocalRewriting,
ClosureConversion,
StateMachineRewriting,
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 3, 2025

Choose a reason for hiding this comment

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

StateMachineRewriting

Would it make sense to make things more granular and split this one into "IteratorRewriting" and "AsyncRewriting"? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think we can. There can be yields remaining after we passed the stage of doing iterator rewriting. Such yields are handled by async/async-iterator rewriter.

LocalRewriting,
ClosureConversion,
StateMachineRewriting,
All
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 3, 2025

Choose a reason for hiding this comment

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

All

"All" feels somewhat strange. This enum is not a [Flags] enum. And returning All from DoesNotSurvive also feels strange. It can be interpreted as "node cannot survive any phase". Only after examining enum underlying values and DoesNotSurvive call sites it becomes clear that the meaning is the opposite. Perhaps "Emit" name would be clearer. It is true that emit phase is not a rewrite phase, but we could pretend that result of "Emit" is an empty tree. #Closed

{

RoslynDebug.Assert(binder is object, "Field 'binder' cannot be null (make the type nullable in BoundNodes.xml to remove this check)");
RoslynDebug.Assert(valueSymbol is object, "Field 'valueSymbol' cannot be null (make the type nullable in BoundNodes.xml to remove this check)");
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 3, 2025

Choose a reason for hiding this comment

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

ug.Assert(valueSy

This feels strange, see a comment for the enum member declaration. #Closed

}

codeCoverageSpans = codeCoverageInstrumenter?.DynamicAnalysisSpans ?? ImmutableArray<SourceSpan>.Empty;
PipelinePhaseValidator.AssertAfterLocalRewriting(loweredStatement);
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 3, 2025

Choose a reason for hiding this comment

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

PipelinePhaseValidator.AssertAfterLocalRewriting(loweredStatement);

It feels like this should be done right after var loweredStatement = localRewriter.VisitStatement(statement); and SpillSequenceSpiller.Rewrite should have its own phase
#Closed

Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly, it feels like localRewriter.AssertNoPlaceholderReplacements(); should be done earlier too.

/// See PipelinePhase enum
/// </summary>
[XmlAttribute]
public string DoesNotSurvive;
}

public class Kind
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 3, 2025

Choose a reason for hiding this comment

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

Kind

Is this type used? #Closed


internal sealed partial class PipelinePhaseValidator
{
private PipelinePhaseValidator()
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 3, 2025

Choose a reason for hiding this comment

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

private PipelinePhaseValidator()

Would it make sense to make the type static? #Closed

Copy link
Contributor

Choose a reason for hiding this comment

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

I see below that we are actually creating instances of this type. Then why do we need this constructor?

All
}

internal sealed partial class PipelinePhaseValidator : BoundTreeWalkerWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 3, 2025

Choose a reason for hiding this comment

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

class PipelinePhaseValidator

Splitting declaration in the same file feels unexpected. Consider defining enum at the top and merging declarations #Closed

Copy link
Contributor

Choose a reason for hiding this comment

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

Consider also following a general pattern for member declarations: first fields, then constructors, then the rest.

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 3, 2025

Done with review pass (commit 2) #Closed

@AlekseyTs
Copy link
Contributor

I didn't make a mistake regarding "initial binding" in my original comment.


In reply to: 3608074660


Refers to: src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml:305 in b5ae4aa. [](commit_id = b5ae4aa, deletion_comment = False)

@jcouv
Copy link
Member Author

jcouv commented Dec 3, 2025

My mistake


In reply to: 3609153340


Refers to: src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml:305 in b5ae4aa. [](commit_id = b5ae4aa, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 3, 2025

Done with review pass (commit 4) #Closed

@jcouv jcouv requested a review from AlekseyTs December 4, 2025 17:20
Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 5)

#endif

#if DEBUG
private PipelinePhaseValidator(PipelinePhase completedPhase)
Copy link
Member

Choose a reason for hiding this comment

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

Should we have a private constructor outside DEBUG too, perhaps a throwing one? (I think you've added it previously, but it seems removed now.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Aleksey asked why have the constructor.
It's not necessary, but the benefit is it would help avoid misuse of the type. But I'm not too worried about that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants