Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,27 @@ BoundExpression rewriteAssignment(BoundExpression leftRead, BoundExpression righ
}

Debug.Assert(TypeSymbol.Equals(transformedLHS.Type, node.Left.Type, TypeCompareKind.AllIgnoreOptions));

if (IsNewExtensionMemberAccessWithByValPossiblyStructReceiver(transformedLHS))
{
// We need to create a tree that ensures that receiver of 'set' is evaluated after the binary operation
BoundLocal binaryResult = _factory.StoreToTemp(opFinal, out BoundAssignmentOperator assignmentToTemp, refKind: RefKind.None);
BoundExpression assignment = MakeAssignmentOperator(syntax, transformedLHS, binaryResult, used: used, isChecked: isChecked, isCompoundAssignment: true);
Debug.Assert(assignment.Type is { });
return new BoundSequence(syntax, [binaryResult.LocalSymbol], [assignmentToTemp], assignment, assignment.Type);
}
Copy link
Member

@jcouv jcouv Jul 14, 2025

Choose a reason for hiding this comment

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

Just to confirm my understanding:
When handling a scenario like x.P += rhs with a struct receiver and by-value receiver parameter, we capture the receiver by reference and after local rewriting and extension rewriting we'd get:

ref S y = ref x
set_P(y, op_Binary(get_P(y), rhs))

But we want:

ref S y = ref x
var binaryResult = op_Binary(get_P(y), rhs);
set_P(y, binaryResult)

That way the derference of y for set_P is done later.
This behaves like x.P += rhs for an instance property, in that the setter can see getter or operator mutations to x.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct.


return MakeAssignmentOperator(syntax, transformedLHS, opFinal, used: used, isChecked: isChecked, isCompoundAssignment: true);
}
}

private BoundExpression? TransformPropertyOrEventReceiver(Symbol propertyOrEvent, BoundExpression? receiverOpt, bool isRegularCompoundAssignment, ArrayBuilder<BoundExpression> stores, ArrayBuilder<LocalSymbol> temps)
private static bool IsNewExtensionMemberAccessWithByValPossiblyStructReceiver(BoundExpression transformedLHS)
{
return transformedLHS is BoundPropertyAccess { PropertySymbol: { } property } && property.GetIsNewExtensionMember() &&
property.ContainingType.ExtensionParameter is { RefKind: RefKind.None, Type.IsReferenceType: false };
}

private BoundExpression? TransformPropertyOrEventReceiver(Symbol propertyOrEvent, BoundExpression? receiverOpt, ArrayBuilder<BoundExpression> stores, ArrayBuilder<LocalSymbol> temps)
{
Debug.Assert(propertyOrEvent.Kind == SymbolKind.Property || propertyOrEvent.Kind == SymbolKind.Event);

Expand Down Expand Up @@ -272,7 +288,9 @@ BoundExpression rewriteAssignment(BoundExpression leftRead, BoundExpression righ
// SPEC VIOLATION: However, for compatibility with Dev12 we will continue treating all generic type parameters, constrained or not,
// SPEC VIOLATION: as value types.
Debug.Assert(rewrittenReceiver.Type is { });
var variableRepresentsLocation = rewrittenReceiver.Type.IsValueType || rewrittenReceiver.Type.Kind == SymbolKind.TypeParameter;
var variableRepresentsLocation = propertyOrEvent.GetIsNewExtensionMember() ?
!rewrittenReceiver.Type.IsReferenceType :
(rewrittenReceiver.Type.IsValueType || rewrittenReceiver.Type.Kind == SymbolKind.TypeParameter);

var receiverTemp = _factory.StoreToTemp(
rewrittenReceiver,
Expand All @@ -286,9 +304,10 @@ BoundExpression rewriteAssignment(BoundExpression leftRead, BoundExpression righ
stackLocalsOpt: null));
temps.Add(receiverTemp.LocalSymbol);

if (!isRegularCompoundAssignment &&
receiverTemp.LocalSymbol.IsRef &&
CodeGenerator.IsPossibleReferenceTypeReceiverOfConstrainedCall(receiverTemp) &&
if (receiverTemp.LocalSymbol.IsRef &&
(propertyOrEvent.GetIsNewExtensionMember() ?
!receiverTemp.Type.IsValueType :
CodeGenerator.IsPossibleReferenceTypeReceiverOfConstrainedCall(receiverTemp)) &&
!CodeGenerator.ReceiverIsKnownToReferToTempIfReferenceType(receiverTemp))
{
BoundAssignmentOperator? extraRefInitialization;
Expand Down Expand Up @@ -678,7 +697,7 @@ private BoundExpression TransformCompoundAssignmentLHS(BoundExpression originalL
{
// This is a temporary object that will be rewritten away before the lowering completes.
return propertyAccess.Update(TransformPropertyOrEventReceiver(propertyAccess.PropertySymbol, propertyAccess.ReceiverOpt,
isRegularCompoundAssignment, stores, temps),
stores, temps),
propertyAccess.InitialBindingReceiverIsSubjectToCloning, propertyAccess.PropertySymbol, propertyAccess.AutoPropertyAccessorKind, propertyAccess.ResultKind, propertyAccess.Type);
}
}
Expand Down Expand Up @@ -804,10 +823,9 @@ private BoundExpression TransformCompoundAssignmentLHS(BoundExpression originalL

if (eventAccess.EventSymbol.IsWindowsRuntimeEvent)
{
Debug.Assert(!isRegularCompoundAssignment);
// This is a temporary object that will be rewritten away before the lowering completes.
return eventAccess.Update(TransformPropertyOrEventReceiver(eventAccess.EventSymbol, eventAccess.ReceiverOpt,
isRegularCompoundAssignment, stores, temps),
stores, temps),
eventAccess.EventSymbol, eventAccess.IsUsableAsField, eventAccess.ResultKind, eventAccess.Type);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,20 @@ BoundExpression rewriteNullCoalscingAssignmentStandard()
// See CodeGenNullCoalescingAssignmentTests.CoalescingAssignment_DynamicRuntimeCastFailure, which will fail if
// isCompoundAssignment is set to true. It will fail to throw a runtime binder cast exception.
Debug.Assert(TypeSymbol.Equals(transformedLHS.Type, node.LeftOperand.Type, TypeCompareKind.AllIgnoreOptions));
BoundExpression assignment = MakeAssignmentOperator(syntax, transformedLHS, loweredRight, used: true, isChecked: false, isCompoundAssignment: false);
BoundExpression assignment;

if (IsNewExtensionMemberAccessWithByValPossiblyStructReceiver(transformedLHS))
{
// We need to create a tree that ensures that receiver of 'set' is evaluated after the right hand side value
BoundLocal rightResult = _factory.StoreToTemp(loweredRight, out BoundAssignmentOperator assignmentToTemp, refKind: RefKind.None);
assignment = MakeAssignmentOperator(syntax, transformedLHS, rightResult, used: true, isChecked: false, isCompoundAssignment: false);
Debug.Assert(assignment.Type is { });
assignment = new BoundSequence(syntax, [rightResult.LocalSymbol], [assignmentToTemp], assignment, assignment.Type);
}
else
{
assignment = MakeAssignmentOperator(syntax, transformedLHS, loweredRight, used: true, isChecked: false, isCompoundAssignment: false);
}

// lhsRead ?? (transformedLHS = loweredRight)
var leftPlaceholder = new BoundValuePlaceholder(lhsRead.Syntax, lhsRead.Type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ public BoundExpression VisitBuiltInOrStaticIncrementOperator(BoundIncrementOpera
//
if (isIndirectOrInstanceField(transformedLHS))
{
Debug.Assert(transformedLHS is not BoundPropertyAccess);
return rewriteWithRefOperand(isPrefix, isChecked, tempSymbols, tempInitializers, syntax, transformedLHS, boundTemp, newValue);
}
else
Expand Down Expand Up @@ -660,16 +661,27 @@ BoundExpression rewriteWithNotRefOperand(

// prefix: temp = (X)(T.Increment((T)operand))); operand = temp;
// postfix: temp = operand; operand = (X)(T.Increment((T)temp)));
ImmutableArray<BoundExpression> assignments = ImmutableArray.Create<BoundExpression>(
MakeAssignmentOperator(syntax, boundTemp, isPrefix ? newValue : MakeRValue(transformedLHS), used: false, isChecked: isChecked, isCompoundAssignment: false),
MakeAssignmentOperator(syntax, transformedLHS, isPrefix ? boundTemp : newValue, used: false, isChecked: isChecked, isCompoundAssignment: false));
tempInitializers.Add(MakeAssignmentOperator(syntax, boundTemp, isPrefix ? newValue : MakeRValue(transformedLHS), used: false, isChecked: isChecked, isCompoundAssignment: false));

if (!isPrefix && IsNewExtensionMemberAccessWithByValPossiblyStructReceiver(transformedLHS))
{
// We need to create a tree that ensures that receiver of 'set' is evaluated after the increment operation
BoundLocal incrementResult = _factory.StoreToTemp(newValue, out BoundAssignmentOperator assignmentToTemp, refKind: RefKind.None);
tempSymbols.Add(incrementResult.LocalSymbol);
tempInitializers.Add(assignmentToTemp);
tempInitializers.Add(MakeAssignmentOperator(syntax, transformedLHS, incrementResult, used: false, isChecked: isChecked, isCompoundAssignment: false));
}
else
{
tempInitializers.Add(MakeAssignmentOperator(syntax, transformedLHS, isPrefix ? boundTemp : newValue, used: false, isChecked: isChecked, isCompoundAssignment: false));
}

// prefix: Seq( operand initializers; temp = (T)(operand + 1); operand = temp; result: temp)
// postfix: Seq( operand initializers; temp = operand; operand = (T)(temp + 1); result: temp)
return new BoundSequence(
syntax: syntax,
locals: tempSymbols.ToImmutableAndFree(),
sideEffects: tempInitializers.ToImmutableAndFree().Concat(assignments),
sideEffects: tempInitializers.ToImmutableAndFree(),
value: boundTemp,
type: boundTemp.Type);
}
Expand Down
54 changes: 28 additions & 26 deletions src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenOperators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5228,41 +5228,43 @@ public static void Repro2(T arg)
1");
compilation.VerifyIL("test<T>.Repro1(T)", @"
{
// Code size 88 (0x58)
// Code size 86 (0x56)
.maxstack 4
.locals init (T& V_0)
.locals init (T& V_0,
T V_1)
IL_0000: ldarg.0
IL_0001: box ""T""
IL_0006: dup
IL_0007: ldfld ""int c0.x""
IL_000c: ldc.i4.1
IL_000d: add
IL_000e: stfld ""int c0.x""
IL_0013: ldarga.s V_0
IL_0015: stloc.0
IL_0016: ldloc.0
IL_0017: ldobj ""T""
IL_001c: box ""T""
IL_0021: ldloc.0
IL_0022: constrained. ""T""
IL_0028: callvirt ""int c0.P1.get""
IL_002d: ldc.i4.1
IL_002e: add
IL_002f: callvirt ""void c0.P1.set""
IL_0034: ldarga.s V_0
IL_0036: stloc.0
IL_0037: ldloc.0
IL_0038: ldobj ""T""
IL_003d: box ""T""
IL_0013: ldarg.0
IL_0014: stloc.1
IL_0015: ldloca.s V_1
IL_0017: stloc.0
IL_0018: ldloc.0
IL_0019: ldloc.0
IL_001a: constrained. ""T""
IL_0020: callvirt ""int c0.P1.get""
IL_0025: ldc.i4.1
IL_0026: add
IL_0027: constrained. ""T""
IL_002d: callvirt ""void c0.P1.set""
IL_0032: ldarga.s V_0
IL_0034: stloc.0
IL_0035: ldloc.0
IL_0036: ldobj ""T""
IL_003b: box ""T""
IL_0040: ldc.i4.1
IL_0041: ldloc.0
IL_0042: ldc.i4.1
IL_0043: ldloc.0
IL_0044: ldc.i4.1
IL_0045: constrained. ""T""
IL_004b: callvirt ""int c0.this[int].get""
IL_0050: ldc.i4.1
IL_0051: add
IL_0052: callvirt ""void c0.this[int].set""
IL_0057: ret
IL_0043: constrained. ""T""
IL_0049: callvirt ""int c0.this[int].get""
IL_004e: ldc.i4.1
IL_004f: add
IL_0050: callvirt ""void c0.this[int].set""
IL_0055: ret
}
").VerifyIL("test<T>.Repro2(T)", @"
{
Expand Down
Loading
Loading