Skip to content

Commit 355f09b

Browse files
authored
Merge pull request #2826 from DavidObando/fix/issue-2818
Fix nested struct field assignment emission
2 parents 71a7964 + e38f523 commit 355f09b

14 files changed

Lines changed: 558 additions & 40 deletions

src/Core/CodeAnalysis/Binding/Binder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2126,6 +2126,7 @@ private static void BindStaticInitializerBlocks(
21262126
{
21272127
IsStatic = true,
21282128
StaticOwnerType = structSym,
2129+
IsStaticInitializer = true,
21292130
};
21302131

21312132
var previousPackage = parentScope.SetCurrentDeclaringPackage(structSym.PackageName);

src/Core/CodeAnalysis/Binding/DeclarationBinder.Structs.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3331,15 +3331,26 @@ private void BindDeferredFieldInitializers(
33313331
// Bind `shared` static field initializers.
33323332
if (staticFieldInitializers.Count > 0)
33333333
{
3334-
var staticInitBuilder = ImmutableDictionary.CreateBuilder<FieldSymbol, BoundExpression>();
3335-
foreach (var (fieldSym, initSyntax, fieldType) in staticFieldInitializers)
3334+
var previousFunction = getCurrentFunction();
3335+
var staticInitializerContext = CreateFieldInitializerAccessibilityContext(structSymbol);
3336+
staticInitializerContext.IsStaticInitializer = true;
3337+
setCurrentFunction(staticInitializerContext);
3338+
try
33363339
{
3337-
var boundInit = bindExpression(initSyntax);
3338-
var convertedInit = conversions.BindConversion(initSyntax.Location, boundInit, fieldType);
3339-
staticInitBuilder[fieldSym] = convertedInit;
3340-
}
3340+
var staticInitBuilder = ImmutableDictionary.CreateBuilder<FieldSymbol, BoundExpression>();
3341+
foreach (var (fieldSym, initSyntax, fieldType) in staticFieldInitializers)
3342+
{
3343+
var boundInit = bindExpression(initSyntax);
3344+
var convertedInit = conversions.BindConversion(initSyntax.Location, boundInit, fieldType);
3345+
staticInitBuilder[fieldSym] = convertedInit;
3346+
}
33413347

3342-
structSymbol.SetStaticFieldInitializers(staticInitBuilder.ToImmutable());
3348+
structSymbol.SetStaticFieldInitializers(staticInitBuilder.ToImmutable());
3349+
}
3350+
finally
3351+
{
3352+
setCurrentFunction(previousFunction);
3353+
}
33433354
}
33443355

33453356
// Bind instance field initializers. These run before the constructor

src/Core/CodeAnalysis/Binding/ExpressionBinder.Assignments.cs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,44 @@ private bool ReceiverBlocksValueTypeMemberWrite(BoundExpression receiver)
365365
&& !ReceiverTypeIsReference(bve.Variable.Type);
366366
}
367367

368+
private bool IsWritableStructFieldReceiver(BoundExpression receiver)
369+
{
370+
if (receiver is BoundVariableExpression)
371+
{
372+
return !ReceiverBlocksValueTypeMemberWrite(receiver);
373+
}
374+
375+
if (receiver is BoundDereferenceExpression dereference)
376+
{
377+
return TypeSymbol.IsUnmanagedPointer(dereference.Operand.Type);
378+
}
379+
380+
if (receiver is not BoundFieldAccessExpression fieldAccess)
381+
{
382+
return false;
383+
}
384+
385+
if (fieldAccess.Receiver == null)
386+
{
387+
var staticConstructorOwner = function?.IsStaticInitializer == true
388+
? function.StaticOwnerType as Symbol
389+
: null;
390+
return fieldAccess.Field.IsStaticAddressLegal(staticConstructorOwner);
391+
}
392+
393+
if (fieldAccess.Field.IsReadOnly
394+
&& !IsReadOnlyFieldAssignmentAllowed(
395+
fieldAccess.Field,
396+
fieldAccess.StructType,
397+
ReceiverExpressionIsThis(fieldAccess.Receiver)))
398+
{
399+
return false;
400+
}
401+
402+
return ReceiverTypeIsReference(fieldAccess.Receiver.Type)
403+
|| IsWritableStructFieldReceiver(fieldAccess.Receiver);
404+
}
405+
368406
/// <summary>
369407
/// Issue #947: returns <see langword="true"/> when the bound
370408
/// <paramref name="receiver"/> denotes the enclosing function's <c>this</c>
@@ -937,9 +975,21 @@ private BoundExpression BindFieldAssignmentExpression(FieldAssignmentExpressionS
937975
$"{structSymbol.Name}.{field.Name}");
938976

939977
var converted = conversions.BindConversion(syntax.Value.Location, BindValue(field.Type), field.Type);
940-
if (implicitFieldReceiverExpr != null
978+
var usesExpressionReceiver = implicitFieldReceiverExpr != null
941979
|| (assignmentReceiver is BoundVariableExpression narrowedVariable && narrowedVariable.NarrowedType != null)
942-
|| assignmentReceiver is BoundUnaryExpression)
980+
|| assignmentReceiver is BoundUnaryExpression;
981+
if (usesExpressionReceiver
982+
&& !structSymbol.IsClass
983+
&& !IsWritableStructFieldReceiver(assignmentReceiver))
984+
{
985+
Diagnostics.ReportFieldAssignmentThroughStructTemporary(
986+
syntax.EqualsToken.Location,
987+
field.Name,
988+
structSymbol);
989+
return new BoundErrorExpression(syntax);
990+
}
991+
992+
if (usesExpressionReceiver)
943993
{
944994
return BoundFieldAssignmentExpression.WithExpressionReceiver(null, assignmentReceiver, structSymbol, field, converted);
945995
}
@@ -1345,6 +1395,15 @@ private BoundExpression TryBindChainedCompoundAssignment(
13451395
Diagnostics.ReportCannotAssign(syntax.OperatorToken.Location, memberName);
13461396
}
13471397

1398+
if (!structSym.IsClass && !IsWritableStructFieldReceiver(boundReceiver))
1399+
{
1400+
Diagnostics.ReportFieldAssignmentThroughStructTemporary(
1401+
syntax.OperatorToken.Location,
1402+
field.Name,
1403+
structSym);
1404+
return new BoundErrorExpression(syntax);
1405+
}
1406+
13481407
var leftRead = new BoundFieldAccessExpression(null, boundReceiver, declaringType, field);
13491408
var binary = TryBindCompoundBinaryOperation(baseOpSyntaxKind, leftRead, boundRhs, syntax.Value.Location);
13501409
if (binary == null)
@@ -1976,6 +2035,15 @@ private BoundExpression BindMemberFieldAssignmentExpression(MemberFieldAssignmen
19762035
$"{declaringType.Name}.{field.Name}");
19772036

19782037
var converted = conversions.BindConversion(syntax.Value.Location, BindValue(field.Type), field.Type);
2038+
if (!structSym.IsClass && !IsWritableStructFieldReceiver(receiver))
2039+
{
2040+
Diagnostics.ReportFieldAssignmentThroughStructTemporary(
2041+
syntax.EqualsToken.Location,
2042+
field.Name,
2043+
structSym);
2044+
return new BoundErrorExpression(syntax);
2045+
}
2046+
19792047
return BoundFieldAssignmentExpression.WithExpressionReceiver(null, receiver, declaringType, field, converted);
19802048
}
19812049

src/Core/CodeAnalysis/DiagnosticBag.Reports.Statements.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@ public void ReportMissingReturnExpression(TextLocation location, TypeSymbol retu
7575
public void ReportCannotAssign(TextLocation location, string name)
7676
=> Report(location, DiagnosticDescriptors.CannotAssign, name);
7777

78+
/// <summary>
79+
/// Reports a field write through a non-addressable struct value, where the
80+
/// write would otherwise target a temporary copy and be discarded.
81+
/// </summary>
82+
/// <param name="location">The assignment operator location.</param>
83+
/// <param name="fieldName">The field being assigned.</param>
84+
/// <param name="receiverType">The struct receiver type.</param>
85+
public void ReportFieldAssignmentThroughStructTemporary(
86+
TextLocation location,
87+
string fieldName,
88+
TypeSymbol receiverType)
89+
=> Report(location, DiagnosticDescriptors.FieldAssignmentThroughStructTemporary, fieldName, receiverType);
90+
7891
/// <summary>
7992
/// Issue #946: reports that an <c>init</c>-only property was assigned
8093
/// outside of object initialization. An <c>init</c>-only property may only

src/Core/CodeAnalysis/DiagnosticDescriptors.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ internal static class DiagnosticDescriptors
369369
internal static readonly DiagnosticDescriptor AmbiguousSourceType = new("GS0496", DiagnosticSeverity.Error, "Type '{0}' is ambiguous between two or more imported packages that each declare a same-named type; qualify the reference with its package name to disambiguate (issue #2455).");
370370
internal static readonly DiagnosticDescriptor AsyncIteratorFunctionLiteralNotSupported = new("GS0497", DiagnosticSeverity.Error, "Async iterator function literals returning '{0}' are not supported; declare a named async iterator function and reference it instead.");
371371
internal static readonly DiagnosticDescriptor GotoIntoExceptionHandler = new("GS0498", DiagnosticSeverity.Error, "A goto cannot enter a catch or finally handler at label '{0}'.");
372+
internal static readonly DiagnosticDescriptor FieldAssignmentThroughStructTemporary = new("GS0499", DiagnosticSeverity.Error, "Cannot modify field '{0}' of struct type '{1}' because the receiver is not writable storage. Store the receiver in a mutable variable first.");
372373
internal static readonly DiagnosticDescriptor CannotTakeAddressOfNonLvalue = new("GS9001", DiagnosticSeverity.Error, "Cannot take address of '{0}': expression is not an lvalue.");
373374
internal static readonly DiagnosticDescriptor ArgumentMustBePassedByRef = new("GS9002", DiagnosticSeverity.Error, "Argument {0} to '{1}' must be passed by reference (`&`).");
374375
internal static readonly DiagnosticDescriptor VariableNotDefinitelyAssignedForRef = new("GS9003", DiagnosticSeverity.Error, "Variable '{0}' must be definitely assigned before being passed by `ref`.");

src/Core/CodeAnalysis/Emit/MethodBodyEmitter.MemberAccess.cs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ private void EmitFieldAccess(BoundFieldAccessExpression fa)
534534
// ADR-0122 §4 / issue #1034: `(*p).field` / `p->field` read. The
535535
// pointer value IS the struct's address, so load the pointer and
536536
// `ldfld` directly — avoiding a wasteful `ldobj` of the whole struct.
537-
this.EmitExpression(deref.Operand);
537+
this.EmitInstanceReceiver(deref.Operand);
538538
}
539539
else if (!receiverIsClass && fa.Receiver is BoundVariableExpression bv && this.TryLoadStructVariableAddress(bv))
540540
{
@@ -662,14 +662,11 @@ private void EmitFieldAssignment(BoundFieldAssignmentExpression fas)
662662
+ "Check AssignmentValueSpillCollector and its ancestor walker.");
663663
}
664664

665-
// Issue #1688: this receiver is a plain value push for `stfld`
666-
// (not an address), so needAddress: false — TryEmitCachedReceiver
667-
// falls back to a normal EmitExpression when no compound-reuse
668-
// slot was planned (the common #1614 simple-assignment case).
669-
if (!this.TryEmitCachedReceiver(addressReceiver ?? fas.ReceiverExpression, needAddress: false))
670-
{
671-
this.EmitExpression(addressReceiver ?? fas.ReceiverExpression);
672-
}
665+
// Issue #2818: stfld needs an object reference for a class
666+
// receiver, but a managed pointer for a value-type receiver.
667+
// EmitInstanceReceiver preserves both shapes and routes nested
668+
// addressable field chains through the shared-receiver cache.
669+
this.EmitInstanceReceiver(addressReceiver ?? fas.ReceiverExpression);
673670

674671
// Issue #1235 (object-initializer follow-up): a `T{Field: value}`
675672
// literal on a class-constrained type parameter lowers to an
@@ -1781,7 +1778,7 @@ private void EmitFieldAddress(BoundFieldAccessExpression fa)
17811778
{
17821779
// ADR-0122 §4/§10: `(*p).field` / `p->field`. The pointer value IS
17831780
// the struct's address, so load the pointer directly before ldflda.
1784-
this.EmitExpression(deref.Operand);
1781+
this.EmitInstanceReceiver(deref.Operand);
17851782
}
17861783
else if (!receiverIsClass && fa.Receiver is BoundVariableExpression bv && this.TryLoadStructVariableAddress(bv))
17871784
{
@@ -1796,7 +1793,11 @@ private void EmitFieldAddress(BoundFieldAccessExpression fa)
17961793
}
17971794
else
17981795
{
1799-
this.EmitExpression(fa.Receiver);
1796+
// Issue #2818: an addressable value-type field chain may have a
1797+
// side-effecting reference-type root shared by compound read/write
1798+
// (`GetHolder().Value.Id += 5`). Route the owner through the
1799+
// receiver cache instead of evaluating that root again.
1800+
this.EmitInstanceReceiver(fa.Receiver);
18001801
}
18011802

18021803
this.il.OpCode(ILOpCode.Ldflda);

src/Core/CodeAnalysis/Emit/ReflectionMetadataEmitter.cs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4299,22 +4299,7 @@ private bool CanLoadVariableAddressForReceiverSpill(
42994299
/// <param name="field">The field whose address is being considered.</param>
43004300
/// <returns><c>true</c> when <c>ldsflda</c> of the field is legal here.</returns>
43014301
internal bool IsStaticFieldAddressLegalHere(FieldSymbol field)
4302-
{
4303-
if (!(field.IsReadOnly && field.IsStatic) || field.IsConst)
4304-
{
4305-
return true;
4306-
}
4307-
4308-
switch (this.emitCtx.CurrentStaticConstructorOwner)
4309-
{
4310-
case StructSymbol s:
4311-
return !s.StaticFields.IsDefaultOrEmpty && s.StaticFields.Contains(field);
4312-
case InterfaceSymbol i:
4313-
return !i.StaticFields.IsDefaultOrEmpty && i.StaticFields.Contains(field);
4314-
default:
4315-
return false;
4316-
}
4317-
}
4302+
=> field.IsStaticAddressLegal(this.emitCtx.CurrentStaticConstructorOwner);
43184303

43194304
private bool IsAddressableFieldAccessForReceiverSpill(
43204305
BoundFieldAccessExpression fa,

src/Core/CodeAnalysis/Emit/SlotPlanner.cs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,8 +1022,9 @@ protected override void VisitClrPropertyAssignmentExpression(BoundClrPropertyAss
10221022
// the shape TryBindChainedCompoundAssignment produces for
10231023
// `getObj().Field += x` (the compound RHS embeds a
10241024
// BoundFieldAccessExpression over the identical receiver instance) —
1025-
// must spill the receiver to a temp and evaluate it exactly once
1026-
// instead of once for the read and once more for the write.
1025+
// must preserve receiver identity across read and write. Reference
1026+
// receivers are cached directly; addressable value-type fields cache
1027+
// their owning receiver so the write still targets original storage.
10271028
protected override void VisitFieldAssignmentExpression(BoundFieldAssignmentExpression node)
10281029
{
10291030
if (node.ReceiverExpression != null)
@@ -1163,10 +1164,37 @@ private void AddIfCompoundReused(BoundExpression receiver, BoundExpression value
11631164
}
11641165

11651166
var found = ReceiverReuseWalker.ContainsReceiverReference(value, receiver);
1166-
if (found)
1167+
if (!found)
11671168
{
1168-
this.sink.Add(receiver);
1169+
return;
1170+
}
1171+
1172+
// Pointer field syntax unwraps `*p` before emit. Cache the pointer
1173+
// operand itself so both the field read and write reuse one value.
1174+
if (receiver is BoundDereferenceExpression dereference
1175+
&& TypeSymbol.IsUnmanagedPointer(dereference.Operand.Type))
1176+
{
1177+
this.sink.Add(dereference.Operand);
1178+
return;
1179+
}
1180+
1181+
// An addressable value-type field must stay in its owning storage:
1182+
// caching the field value would turn the write into a temp-local
1183+
// mutation. Cache the side-effecting owner instead, then let
1184+
// EmitFieldAddress rebuild the same address for read and write.
1185+
if (receiver is BoundFieldAccessExpression fieldAccess
1186+
&& ReflectionMetadataEmitter.IsValueTypeSymbol(receiver.Type)
1187+
&& !this.needsRvalueReceiverSpill(receiver, this.function, this.locals))
1188+
{
1189+
if (fieldAccess.Receiver != null)
1190+
{
1191+
this.AddIfCompoundReused(fieldAccess.Receiver, value);
1192+
}
1193+
1194+
return;
11691195
}
1196+
1197+
this.sink.Add(receiver);
11701198
}
11711199
}
11721200

src/Core/CodeAnalysis/Symbols/FieldSymbol.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,25 @@ public void SetFixedBuffer(TypeSymbol elementType, int length)
129129
FixedBufferElementType = elementType;
130130
FixedBufferLength = length;
131131
}
132+
133+
/// <summary>
134+
/// Returns whether this static field may be addressed in the current
135+
/// static-constructor context.
136+
/// </summary>
137+
/// <param name="staticConstructorOwner">The type whose <c>.cctor</c> is active, or <c>null</c>.</param>
138+
/// <returns><c>true</c> when taking the field address is legal.</returns>
139+
internal bool IsStaticAddressLegal(Symbol staticConstructorOwner)
140+
{
141+
if (!(IsReadOnly && IsStatic) || IsConst)
142+
{
143+
return true;
144+
}
145+
146+
return staticConstructorOwner switch
147+
{
148+
StructSymbol s => !s.StaticFields.IsDefaultOrEmpty && s.StaticFields.Contains(this),
149+
InterfaceSymbol i => !i.StaticFields.IsDefaultOrEmpty && i.StaticFields.Contains(this),
150+
_ => false,
151+
};
152+
}
132153
}

src/Core/CodeAnalysis/Symbols/FunctionSymbol.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,9 @@ public ImmutableArray<TypeParameterSymbol> TypeParameters
436436
/// <summary>Gets a value indicating whether this function is a P/Invoke stub (ADR-0086).</summary>
437437
public bool IsPInvoke => PInvokeMetadata != null;
438438

439+
/// <summary>Gets or sets a value indicating whether this synthetic function represents a type's static-constructor context.</summary>
440+
internal bool IsStaticInitializer { get; set; }
441+
439442
/// <summary>
440443
/// ADR-0105 Phase 2 — re-points this (reused) function symbol at the
441444
/// declaration node of a freshly-parsed syntax tree whose member signature

0 commit comments

Comments
 (0)