Skip to content

Conversation

@CyrusNajmabadi
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi commented Nov 5, 2025

Relates to test plan #80613
Closes #80998

@dotnet-policy-service dotnet-policy-service bot added VSCode Needs UX Triage Needs API Review Needs to be reviewed by the API review council labels Nov 5, 2025

var operation = semanticModel.GetOperation(root.DescendantNodes().OfType<CollectionExpressionSyntax>().ToArray()[1]);
VerifyOperationTree(compilation, operation, """
ICollectionExpressionOperation (1 elements, ConstructMethod: MyList<System.Int32>..ctor([System.Int32 capacity = 0], [System.String name = "default"])) (OperationKind.CollectionExpression, Type: MyList<System.Int32>) (Syntax: '[with(capacity: 10), 2]')
Copy link
Member Author

Choose a reason for hiding this comment

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

review with whitespace off.

{
EvalStackFrame frame = PushStackFrame();
var creationArguments = VisitArguments(operation.CreationArguments, instancePushed: false);
PopStackFrame(frame);
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 seemed sensible to me to push/pop a frame here independent of the frame for visiting the elements. But i wasn't 100% sure. LMK if this is correct.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure what the decision to keep the frames for the arguments and elements "independent" versus nested like in VisitObjectCreation could impact.

Copy link
Member

@jcouv jcouv Nov 10, 2025

Choose a reason for hiding this comment

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

Maybe Aleksey can advise. Otherwise this does need some more research
I suspect we can do await with the split

Copy link
Member Author

Choose a reason for hiding this comment

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

note: my intent was to match VisitObjectCreation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

note: my intent was to match VisitObjectCreation.

Thanks for the clarification
In that case, would it be an outer frame with an inner frame (push, push, pop, pop) instead of two separate frames (push, pop, push, pop)?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe Aleksey can advise.

See my other comments

</summary>
</Comments>
</Property>
<Property Name="CreationArguments" Type="ImmutableArray&lt;IArgumentOperation&gt;">
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 went with CreationArguments as i think it's nicely 'generic'. They are the arguments that control the 'creation' of the collection. I didn't really want to be very explicit that they were the 'WithElement' arguments. But i don't feel strongly on this. If people think it should align more closely with the syntax, that's ok with me.

Copy link
Member

Choose a reason for hiding this comment

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

It feels like ConstructArguments to emphasize the connection with ConstructMethod would be reasonable here. However, I also don't feel strongly. :)

@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review November 5, 2025 20:33
@CyrusNajmabadi CyrusNajmabadi requested review from a team as code owners November 5, 2025 20:33
@CyrusNajmabadi
Copy link
Member Author

@RikkiGibson @jjonescz @333fred ptal.

// (12,74): warning CS8620: Argument of type 'T' cannot be used for parameter 'items' of type 'ReadOnlySpan<T?>' in 'MyCollection<T?> MyCollectionBuilder.Create<T?>(ReadOnlySpan<T?> items)' due to differences in the nullability of reference types.
// static IMyCollection<T?> F<T>(ReadOnlySpan<T> items, T arg) => [with(arg), ..items];
Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "arg").WithArguments("T", "System.ReadOnlySpan<T?>", "items", "MyCollection<T?> MyCollectionBuilder.Create<T?>(ReadOnlySpan<T?> items)").WithLocation(12, 74));
Diagnostic(ErrorCode.ERR_BadCollectionArgumentsArgCount, "with(arg)").WithArguments("Create", "1").WithLocation(12, 69));
Copy link
Member Author

Choose a reason for hiding this comment

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

note: this now matches CollectionBuilder_SpreadElement_BoxingConversion_A, which also has no nullable warning here. the only error we should report is the bad-with, since we only bind to (ReadOnlySpan<T> items) not (ReadOnlySpan<T> items, T arg) (since arguments can't go at builder end, they must be at the start).

@jcouv
Copy link
Member

jcouv commented Nov 6, 2025

Closes #80998 ?

@CyrusNajmabadi
Copy link
Member Author

Assert added in d4da134

ICollectionExpressionOperation (1 elements, ConstructMethod: System.Collections.Generic.List<System.Int32>..ctor(System.Int32 capacity)) (OperationKind.CollectionExpression, Type: System.Collections.Generic.IList<System.Int32>) (Syntax: '[with(Compu ... seBranch()]')
ConstructArguments(1):
IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: capacity) (OperationKind.Argument, Type: null) (Syntax: 'ComputeCapacity()')
IInvocationOperation (System.Int32 Program.ComputeCapacity()) (OperationKind.Invocation, Type: System.Int32) (Syntax: 'ComputeCapacity()')
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 11, 2025

Choose a reason for hiding this comment

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

ComputeCapacity

Note that this is evaluated after TrueBranch and FalseBranch. Should be the other way around #Closed

if (operation.ConstructArguments.Any(a => a is IArgumentOperation) && !operation.ConstructArguments.All(a => a is IArgumentOperation))
throw ExceptionUtilities.UnexpectedValue("Mixed argument operations and non-argument operations in ConstructArguments");

// Ugly, but necessary. If we bound successfully, we'll have an array of IArgumentOperation. We want to
Copy link
Contributor

Choose a reason for hiding this comment

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

Ugly, but necessary.

I do not find this statement useful in general, and, frankly, I do not find the code ugly. We are always trying to do what is necessary to have correct behavior for all possible scenarios.

Copy link
Member Author

Choose a reason for hiding this comment

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

? PopArray(operation.ConstructArguments)
: ImmutableArray<IOperation>.CastUp(PopArray(arguments, RewriteArgumentFromArray));

return PopStackFrame(frame, new CollectionExpressionOperation(
Copy link
Contributor

Choose a reason for hiding this comment

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

return PopStackFrame(frame, new CollectionExpressionOperation(

A change for this line is unnecessary, but creates a "noise" in the diff and history. Please revert since there is no intent to change behavior here.

Copy link
Contributor

Choose a reason for hiding this comment

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

To clarify, this refers to the change around the following original lines:

            PopStackFrame(frame);
            return new CollectionExpressionOperation(

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 find this easier to read and reason about as it prevents anything from sneaking in between the popping of the stack frame and the returning of the final collection expr op. I would prefer to keep it in this form as i think it better represents the semantics wanted here.

// creation methods when producing the .ConstructArguments for the ICollectionExpressionOperation.
// Note: the caller will end up stripping this off when producing the ConstructArguments, so it will
// not actually leak to the user. But this ends up keeping the logic simple between that callsite
// and this code which actually hits all the arguments passed along. Because we never actually
Copy link
Contributor

Choose a reason for hiding this comment

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

I find this approach bug-prone and, at the moment, I am not convinced that it is the right "price" for the simplification

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you have a recommendation on an alternative approach?

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have a recommendation on an alternative approach?

I am assuming this is about ReadOnlySpan placeholder and leaning towards the following:

  • It will be cleaner (and probably simpler) to leave the node in IOperation tree
  • Bound tree should use a placeholder node that cannot be confused with a placeholder used for any other purpose. For example, by adding a placeholder kind to ValuePlaceholder, or by using distinct bound node type.
  • For that node, we probably should create IPlaceholderOperation with new PlaceholderKind, or add a whole new IOperation node
  • Stack spilling in CFG builder should be adjusted to to leave the placeholder on the stack unchanged

if (boundCall.IsErroneousNode)
{
var array = @this.CreateFromArray<BoundNode, IOperation>(((IBoundInvalidNode)boundCall).InvalidNodeChildren);
return array.WhereAsArray(o => o is not IPlaceholderOperation);
Copy link
Contributor

Choose a reason for hiding this comment

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

o is not IPlaceholderOperation

This looks bug-prone, theoretically we could have meaningful placeholders among the arguments. If not now, then in the future.

Copy link
Member Author

Choose a reason for hiding this comment

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

if (collectionCreation is null)
return [];

Debug.Fail("Unhandled case: " + collectionCreation.GetType());
Copy link
Contributor

Choose a reason for hiding this comment

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

Debug.Fail("Unhandled case: " + collectionCreation.GetType());

Please add Validate method to the BoundCollectionExpression node by adding HasValidate="true" attribute in BoundNodes.xml, and assert possible values that could be in CollectionCreation according to this method.

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice. 7bdad0d


// With a CollectionBuilder, the last argument will be a placeholder where the .Elements will go.
// We do *not* want to include that information in the Arguments we return.
Debug.Assert(arguments is [.., IArgumentOperation { Value: IPlaceholderOperation { PlaceholderKind: PlaceholderKind.Unspecified } }], "We should always have at least one argument (the placeholder elements).");
Copy link
Contributor

Choose a reason for hiding this comment

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

IPlaceholderOperation

I assume this is to account for presence of ```CollectionBuilderElementsPlaceholder``. Consider asserting in Validate when and where do we expect it to be found

Copy link
Member Author

Choose a reason for hiding this comment

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

SGTM: 9727a59

@AlekseyTs
Copy link
Contributor

                Span<int> span = [with(in x), 1, 2, 3];

It will be interesting to check the shape of IOperation tree for this collection expression and see how x is reflected in it.


Refers to: src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests_WithElement_ArraysAndSpans.cs:578 in b21f43c. [](commit_id = b21f43c, deletion_comment = False)

@CyrusNajmabadi
Copy link
Member Author

CyrusNajmabadi commented Nov 13, 2025 via email

namespace Microsoft.CodeAnalysis.CSharp.UnitTests;

[CompilerTrait(CompilerFeature.CollectionExpressions)]
public sealed class CollectionExpressionTests_WithElement_IOperation : CSharpTestBase
Copy link
Contributor

Choose a reason for hiding this comment

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

CollectionExpressionTests_WithElement_IOperation

Please move this file to Microsoft.CodeAnalysis.CSharp.IOperation.UnitTests project. It is fine to add IOperation/CFG verification to some of the existing tests to assert more for those scenarios. However, we prefer all tests with the sole purpose to verify IOperation/CFG to be placed to the dedicated test project.

ErrorCode.ERR_CollectionArgumentsNotSupportedForType,
_node.WithElement.Syntax.GetFirstToken().GetLocation(),
_targetType);
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

return null;

We should have a test verifying what happens with WithElement in IOperation tree when we hit this code path.

Copy link
Member Author

Choose a reason for hiding this comment

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

// For the read-only array interfaces (IEnumerable<E>, IReadOnlyCollection<E>, IReadOnlyList<E>), only
// the parameterless `with()` is allowed.
_diagnostics.Add(ErrorCode.ERR_CollectionArgumentsMustBeEmpty, _node.WithElement.Syntax.GetFirstToken().GetLocation());
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

return null;

We should have a test verifying what happens with WithElement in IOperation tree when we hit this code path.

Copy link
Member Author

Choose a reason for hiding this comment

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

@CyrusNajmabadi
Copy link
Member Author

CyrusNajmabadi commented Nov 13, 2025 via email

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 54), only glanced over the test changes

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.

5 participants