/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_FixedStatement.cs
577 строк
28 KB
AlekseyTs
Ensure extension receiver is converted for pattern matching Deconstruct and pattern based `using` scenarios. (#81816)
06 янв 2026, 20:42
Не верифицирован
06 янв 2026, 20:42
7e962b9
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp { internal sealed partial class LocalRewriter { public override BoundNode VisitFixedStatement(BoundFixedStatement node) { ImmutableArray<BoundLocalDeclaration> localDecls = node.Declarations.LocalDeclarations; int numFixedLocals = localDecls.Length; var localBuilder = ArrayBuilder<LocalSymbol>.GetInstance(node.Locals.Length); localBuilder.AddRange(node.Locals); var statementBuilder = ArrayBuilder<BoundStatement>.GetInstance(numFixedLocals + 1 + 1); //+1 for body, +1 for hidden seq point var cleanup = new BoundStatement[numFixedLocals]; for (int i = 0; i < numFixedLocals; i++) { BoundLocalDeclaration localDecl = localDecls[i]; LocalSymbol pinnedTemp; statementBuilder.Add(InitializeFixedStatementLocal(localDecl, _factory, out pinnedTemp)); localBuilder.Add(pinnedTemp); // NOTE: Dev10 nulls out the locals in declaration order (as opposed to "popping" them in reverse order). if (pinnedTemp.RefKind == RefKind.None) { // temp = null; cleanup[i] = _factory.Assignment(_factory.Local(pinnedTemp), _factory.Null(pinnedTemp.Type)); } else { // temp = ref *default(T*); cleanup[i] = _factory.Assignment(_factory.Local(pinnedTemp), _factory.NullRef(pinnedTemp.TypeWithAnnotations), isRef: true); } } BoundStatement? rewrittenBody = VisitStatement(node.Body); Debug.Assert(rewrittenBody is { }); statementBuilder.Add(rewrittenBody); statementBuilder.Add(_factory.HiddenSequencePoint()); Debug.Assert(statementBuilder.Count == numFixedLocals + 1 + 1); // In principle, the cleanup code (i.e. nulling out the pinned variables) is always // in a finally block. However, we can optimize finally away (keeping the cleanup // code) in cases where both of the following are true: // 1) there are no branches out of the fixed statement; and // 2) the fixed statement is not in a try block (syntactic or synthesized). if (IsInTryBlock(node) || HasGotoOut(rewrittenBody)) { return _factory.Block( localBuilder.ToImmutableAndFree(), new BoundTryStatement( _factory.Syntax, _factory.Block(statementBuilder.ToImmutableAndFree()), ImmutableArray<BoundCatchBlock>.Empty, _factory.Block(cleanup))); } else { statementBuilder.AddRange(cleanup); return _factory.Block(localBuilder.ToImmutableAndFree(), statementBuilder.ToImmutableAndFree()); } } /// <summary> /// Basically, what we need to know is, if an exception occurred within the fixed statement, would /// additional code in the current method be executed before its stack frame was popped? /// </summary> private static bool IsInTryBlock(BoundFixedStatement boundFixed) { SyntaxNode? node = boundFixed.Syntax.Parent; Debug.Assert(node is { }); while (node != null) { switch (node.Kind()) { case SyntaxKind.TryStatement: // NOTE: if we started in the catch or finally of this try statement, // we will have bypassed this node. return true; case SyntaxKind.UsingStatement: // ACASEY: In treating using statements as try-finally's, we're following // Dev11. The practical explanation for Dev11's behavior is that using // statements have already been lowered by the time the check is performed. // A more thoughtful explanation is that user code could run between the // raising of an exception and the unwinding of the stack (via Dispose()) // and that user code would likely appreciate the reduced memory pressure // of having the fixed local unpinned. // NOTE: As in Dev11, we're not emitting a try-finally if the fixed // statement is nested within a lock statement. Practically, dev11 // probably lowers locks after fixed statement, and so, does not see // the try-finally. More thoughtfully, no user code will run in the // finally statement, so it's not necessary. // BREAK: Takes into account whether an outer fixed statement will be // lowered into a try-finally block and responds accordingly. This is // unnecessary since nothing will ever be allocated in the finally // block of a lowered fixed statement, so memory pressure is not an // issue. Note that only nested fixed statements where the outer (but // not the inner) fixed statement has an unmatched goto, but is not // contained in a try-finally, will be affected. e.g. // fixed (...) { // fixed (...) { } // goto L1: ; // } return true; case SyntaxKind.ForEachStatement: case SyntaxKind.ForEachVariableStatement: // We're being conservative here - there's actually only // a try block if the enumerator is disposable, but we // can't tell that from the syntax. Dev11 checks in the // lowered tree, so it is more precise. return true; case SyntaxKind.SimpleLambdaExpression: case SyntaxKind.ParenthesizedLambdaExpression: case SyntaxKind.AnonymousMethodExpression: // Stop looking. return false; case SyntaxKind.CatchClause: // If we're in the catch of a try-catch-finally, then // we're still in the scope of the try-finally handler. Debug.Assert(node.Parent is TryStatementSyntax); if (((TryStatementSyntax)node.Parent).Finally != null) { return true; } goto case SyntaxKind.FinallyClause; case SyntaxKind.FinallyClause: // Skip past the enclosing try to avoid a false positive. node = node.Parent; Debug.Assert(node is { } && node.Kind() == SyntaxKind.TryStatement); node = node.Parent; break; default: if (node is MemberDeclarationSyntax) { // Stop looking. return false; } node = node.Parent; break; } } return false; } /// <summary> /// If two (or more) fixed statements are nested, then we want to avoid having the outer /// fixed statement re-traverse the lowered bound tree of the inner one. We accomplish /// this by having each fixed statement cache a set of unmatched gotos that can be /// reused by any containing fixed statements. /// </summary> private Dictionary<BoundNode, HashSet<LabelSymbol>>? _lazyUnmatchedLabelCache; /// <summary> /// Look for gotos without corresponding labels in the lowered body of a fixed statement. /// </summary> /// <remarks> /// Assumes continue, break, etc have already been rewritten to gotos. /// </remarks> private bool HasGotoOut(BoundNode node) { if (_lazyUnmatchedLabelCache == null) { _lazyUnmatchedLabelCache = new Dictionary<BoundNode, HashSet<LabelSymbol>>(); } HashSet<LabelSymbol> unmatched = UnmatchedGotoFinder.Find(node, _lazyUnmatchedLabelCache, RecursionDepth); _lazyUnmatchedLabelCache.Add(node, unmatched); return unmatched != null && unmatched.Count > 0; } public override BoundNode VisitFixedLocalCollectionInitializer(BoundFixedLocalCollectionInitializer node) { throw ExceptionUtilities.Unreachable(); //Should be handled by VisitFixedStatement } private BoundStatement InitializeFixedStatementLocal( BoundLocalDeclaration localDecl, SyntheticBoundNodeFactory factory, out LocalSymbol pinnedTemp) { BoundExpression? initializer = localDecl.InitializerOpt; Debug.Assert(!ReferenceEquals(initializer, null)); LocalSymbol localSymbol = localDecl.LocalSymbol; var fixedCollectionInitializer = (BoundFixedLocalCollectionInitializer)initializer; if (fixedCollectionInitializer.GetPinnableOpt is { }) { return InitializeFixedStatementGetPinnable(localDecl, localSymbol, fixedCollectionInitializer, factory, out pinnedTemp); } else if (fixedCollectionInitializer.Expression.Type is { SpecialType: SpecialType.System_String }) { return InitializeFixedStatementStringLocal(localDecl, localSymbol, fixedCollectionInitializer, factory, out pinnedTemp); } else if (fixedCollectionInitializer.Expression.Type is { TypeKind: TypeKind.Array }) { return InitializeFixedStatementArrayLocal(localDecl, localSymbol, fixedCollectionInitializer, factory, out pinnedTemp); } else { return InitializeFixedStatementRegularLocal(localDecl, localSymbol, fixedCollectionInitializer, factory, out pinnedTemp); } } /// <summary> /// <![CDATA[ /// fixed(int* ptr = &v){ ... } == becomes ===> /// /// pinned ref int pinnedTemp = ref v; // pinning managed ref /// int* ptr = (int*)&pinnedTemp; // unsafe cast to unmanaged ptr /// . . . /// ]]> /// </summary> private BoundStatement InitializeFixedStatementRegularLocal( BoundLocalDeclaration localDecl, LocalSymbol localSymbol, BoundFixedLocalCollectionInitializer fixedInitializer, SyntheticBoundNodeFactory factory, out LocalSymbol pinnedTemp) { TypeSymbol localType = localSymbol.Type; BoundExpression initializerExpr = VisitExpression(fixedInitializer.Expression); Debug.Assert(initializerExpr.Type is { TypeKind: TypeKind.Pointer }); // initializer expr should be either an address(&) of something or a fixed field access. // either should lower into addressof Debug.Assert(initializerExpr.Kind == BoundKind.AddressOfOperator); TypeSymbol initializerType = ((PointerTypeSymbol)initializerExpr.Type).PointedAtType; // initializer expressions are bound/lowered right into addressof operators here // that is a bit too far // we need to pin the underlying field, and only then take the address. initializerExpr = ((BoundAddressOfOperator)initializerExpr).Operand; // intervening parens may have been skipped by the binder; find the declarator VariableDeclaratorSyntax? declarator = fixedInitializer.Syntax.FirstAncestorOrSelf<VariableDeclaratorSyntax>(); Debug.Assert(declarator != null); pinnedTemp = factory.SynthesizedLocal( initializerType, syntax: declarator, isPinned: true, //NOTE: different from the array and string cases // RefReadOnly to allow referring to readonly variables. (technically we only "read" through the temp anyways) refKind: RefKind.RefReadOnly, kind: SynthesizedLocalKind.FixedReference); // NOTE: we pin the reference, not the pointer. Debug.Assert(pinnedTemp.IsPinned); Debug.Assert(!localSymbol.IsPinned); // pinnedTemp = ref v; BoundStatement pinnedTempInit = factory.Assignment(factory.Local(pinnedTemp), initializerExpr, isRef: true); // &pinnedTemp var addr = new BoundAddressOfOperator( factory.Syntax, factory.Local(pinnedTemp), type: fixedInitializer.ElementPointerType); // (int*)&pinnedTemp var pointerValue = ApplyConversionIfNotIdentity(fixedInitializer.ElementPointerConversion, fixedInitializer.ElementPointerPlaceholder, addr); // ptr = (int*)&pinnedTemp; BoundStatement localInit = InstrumentLocalDeclarationIfNecessary(localDecl, localSymbol, factory.Assignment(factory.Local(localSymbol), pointerValue)); return factory.Block(pinnedTempInit, localInit); } /// <summary> /// <![CDATA[ /// fixed(int* ptr = &v){ ... } == becomes ===> /// /// pinned ref int pinnedTemp = ref v; // pinning managed ref /// int* ptr = (int*)&pinnedTemp; // unsafe cast to unmanaged ptr /// . . . /// ]]> /// </summary> private BoundStatement InitializeFixedStatementGetPinnable( BoundLocalDeclaration localDecl, LocalSymbol localSymbol, BoundFixedLocalCollectionInitializer fixedInitializer, SyntheticBoundNodeFactory factory, out LocalSymbol pinnedTemp) { TypeSymbol localType = localSymbol.Type; BoundExpression initializerExpr = VisitExpression(fixedInitializer.Expression); Debug.Assert(initializerExpr.Type is { }); var initializerType = initializerExpr.Type; var initializerSyntax = initializerExpr.Syntax; var getPinnableMethod = fixedInitializer.GetPinnableOpt; Debug.Assert(getPinnableMethod is { }); // intervening parens may have been skipped by the binder; find the declarator VariableDeclaratorSyntax? declarator = fixedInitializer.Syntax.FirstAncestorOrSelf<VariableDeclaratorSyntax>(); Debug.Assert(declarator != null); // pinned ref int pinnedTemp pinnedTemp = factory.SynthesizedLocal( getPinnableMethod.ReturnType, syntax: declarator, isPinned: true, //NOTE: different from the array and string cases // RefReadOnly to allow referring to readonly variables. (technically we only "read" through the temp anyways) refKind: RefKind.RefReadOnly, kind: SynthesizedLocalKind.FixedReference); BoundExpression callReceiver; int currentConditionalAccessID = 0; bool needNullCheck = !initializerType.IsValueType || initializerType.IsNullableType(); BoundAssignmentOperator? nullableTempAssignment = null; BoundLocal? nullableBoundTemp = null; if (needNullCheck) { if (initializerType.IsNullableType()) { nullableBoundTemp = factory.StoreToTemp(initializerExpr, out nullableTempAssignment); callReceiver = nullableBoundTemp; } else { currentConditionalAccessID = ++_currentConditionalAccessID; callReceiver = new BoundConditionalReceiver( initializerSyntax, currentConditionalAccessID, initializerType); } } else { callReceiver = initializerExpr; } // Tracked by https://github.com/dotnet/roslyn/issues/78827 : MQ, Consider preserving the BoundConversion from initial binding instead of using markAsChecked here // .GetPinnable() var getPinnableCall = getPinnableMethod.IsStatic ? factory.Call(null, getPinnableMethod, this.ConvertReceiverForExtensionIfNeeded(callReceiver, markAsChecked: true, getPinnableMethod.Parameters[0])) : factory.Call(this.ConvertReceiverForExtensionMemberIfNeeded(getPinnableMethod, callReceiver, markAsChecked: true), getPinnableMethod); // temp =ref .GetPinnable() var tempAssignment = factory.AssignmentExpression( factory.Local(pinnedTemp), getPinnableCall, isRef: true); // &pinnedTemp var addr = new BoundAddressOfOperator( factory.Syntax, factory.Local(pinnedTemp), type: fixedInitializer.ElementPointerType); // (int*)&pinnedTemp var pointerValue = ApplyConversionIfNotIdentity(fixedInitializer.ElementPointerConversion, fixedInitializer.ElementPointerPlaceholder, addr); // {pinnedTemp =ref .GetPinnable(), (int*)&pinnedTemp} BoundExpression pinAndGetPtr = factory.Sequence( locals: ImmutableArray<LocalSymbol>.Empty, sideEffects: ImmutableArray.Create<BoundExpression>(tempAssignment), result: pointerValue); if (needNullCheck) { if (initializerType.IsNullableType()) { Debug.Assert(nullableBoundTemp is not null); Debug.Assert(nullableTempAssignment is not null); // (nullableTemp = initializer).HasValue ? {temp =ref nullableTemp.GetPinnable(), (int*)&pinnedTemp} : default; pinAndGetPtr = RewriteConditionalOperator( initializerSyntax, rewrittenCondition: factory.MakeNullableHasValue(initializerSyntax, nullableBoundTemp), rewrittenConsequence: pinAndGetPtr, rewrittenAlternative: _factory.Default(localType), constantValueOpt: null, localType, isRef: false); pinAndGetPtr = factory.Sequence( locals: ImmutableArray.Create(nullableBoundTemp.LocalSymbol), sideEffects: ImmutableArray.Create<BoundExpression>(nullableTempAssignment), result: pinAndGetPtr); } else { // initializer?.{temp =ref .GetPinnable(), (int*)&pinnedTemp} ?? default; pinAndGetPtr = new BoundLoweredConditionalAccess( initializerSyntax, initializerExpr, hasValueMethodOpt: null, whenNotNull: pinAndGetPtr, whenNullOpt: null, // just return default(T*) currentConditionalAccessID, forceCopyOfNullableValueType: false, localType); } } // ptr = initializer?.{temp =ref .GetPinnable(), (int*)&pinnedTemp} ?? default; BoundStatement localInit = InstrumentLocalDeclarationIfNecessary(localDecl, localSymbol, factory.Assignment(factory.Local(localSymbol), pinAndGetPtr)); return localInit; } /// <summary> /// fixed(char* ptr = stringVar){ ... } == becomes ===> /// /// pinned string pinnedTemp = stringVar; // pinning managed ref /// char* ptr = (char*)pinnedTemp; // unsafe cast to unmanaged ptr /// if (pinnedTemp != null) ptr += OffsetToStringData(); /// . . . /// </summary> private BoundStatement InitializeFixedStatementStringLocal( BoundLocalDeclaration localDecl, LocalSymbol localSymbol, BoundFixedLocalCollectionInitializer fixedInitializer, SyntheticBoundNodeFactory factory, out LocalSymbol pinnedTemp) { TypeSymbol localType = localSymbol.Type; BoundExpression initializerExpr = VisitExpression(fixedInitializer.Expression); TypeSymbol? initializerType = initializerExpr.Type; Debug.Assert(initializerType is { }); // intervening parens may have been skipped by the binder; find the declarator VariableDeclaratorSyntax? declarator = fixedInitializer.Syntax.FirstAncestorOrSelf<VariableDeclaratorSyntax>(); Debug.Assert(declarator != null); pinnedTemp = factory.SynthesizedLocal( initializerType, syntax: declarator, isPinned: true, kind: SynthesizedLocalKind.FixedReference); // NOTE: we pin the string, not the pointer. Debug.Assert(pinnedTemp.IsPinned); Debug.Assert(!localSymbol.IsPinned); BoundStatement stringTempInit = factory.Assignment(factory.Local(pinnedTemp), initializerExpr); // (char*)pinnedTemp; var addr = factory.Convert( fixedInitializer.ElementPointerType, factory.Local(pinnedTemp), Conversion.PinnedObjectToPointer); var convertedStringTemp = ApplyConversionIfNotIdentity(fixedInitializer.ElementPointerConversion, fixedInitializer.ElementPointerPlaceholder, addr); BoundStatement localInit = InstrumentLocalDeclarationIfNecessary(localDecl, localSymbol, factory.Assignment(factory.Local(localSymbol), convertedStringTemp)); BoundExpression notNullCheck = _factory.MakeNullCheck(factory.Syntax, factory.Local(localSymbol), BinaryOperatorKind.NotEqual); BoundExpression helperCall; MethodSymbol? offsetMethod; if (TryGetWellKnownTypeMember(fixedInitializer.Syntax, WellKnownMember.System_Runtime_CompilerServices_RuntimeHelpers__get_OffsetToStringData, out offsetMethod)) { helperCall = factory.Call(receiver: null, method: offsetMethod); } else { helperCall = new BoundBadExpression(fixedInitializer.Syntax, LookupResultKind.NotInvocable, ImmutableArray<Symbol?>.Empty, ImmutableArray<BoundExpression>.Empty, ErrorTypeSymbol.UnknownResultType); } BoundExpression addition = factory.Binary(BinaryOperatorKind.PointerAndIntAddition, localType, factory.Local(localSymbol), helperCall); BoundStatement conditionalAdd = factory.If(notNullCheck, factory.Assignment(factory.Local(localSymbol), addition)); return factory.Block(stringTempInit, localInit, conditionalAdd); } /// <summary> /// <![CDATA[ /// fixed(int* ptr = arr){ ... } == becomes ===> /// /// pinned int[] pinnedTemp = arr; // pinning managed ref /// int* ptr = pinnedTemp != null && pinnedTemp.Length != 0 ? /// (int*)&pinnedTemp[0] : // unsafe cast to unmanaged ptr /// 0; /// . . . /// ]]> /// </summary> private BoundStatement InitializeFixedStatementArrayLocal( BoundLocalDeclaration localDecl, LocalSymbol localSymbol, BoundFixedLocalCollectionInitializer fixedInitializer, SyntheticBoundNodeFactory factory, out LocalSymbol pinnedTemp) { TypeSymbol localType = localSymbol.Type; BoundExpression initializerExpr = VisitExpression(fixedInitializer.Expression); TypeSymbol? initializerType = initializerExpr.Type; Debug.Assert(initializerType is { }); pinnedTemp = factory.SynthesizedLocal(initializerType, isPinned: true); ArrayTypeSymbol arrayType = (ArrayTypeSymbol)pinnedTemp.Type; TypeWithAnnotations arrayElementType = arrayType.ElementTypeWithAnnotations; // NOTE: we pin the array, not the pointer. Debug.Assert(pinnedTemp.IsPinned); Debug.Assert(!localSymbol.IsPinned); //(pinnedTemp = array) BoundExpression arrayTempInit = factory.AssignmentExpression(factory.Local(pinnedTemp), initializerExpr); //(pinnedTemp = array) != null BoundExpression notNullCheck = _factory.MakeNullCheck(factory.Syntax, arrayTempInit, BinaryOperatorKind.NotEqual); BoundExpression lengthCall; if (arrayType.IsSZArray) { lengthCall = factory.ArrayLength(factory.Local(pinnedTemp)); } else { MethodSymbol? lengthMethod; if (TryGetSpecialTypeMethod(fixedInitializer.Syntax, SpecialMember.System_Array__get_Length, out lengthMethod)) { lengthCall = factory.Call(factory.Local(pinnedTemp), lengthMethod); } else { lengthCall = new BoundBadExpression(fixedInitializer.Syntax, LookupResultKind.NotInvocable, ImmutableArray<Symbol?>.Empty, ImmutableArray.Create<BoundExpression>(factory.Local(pinnedTemp)), ErrorTypeSymbol.UnknownResultType); } } // NOTE: dev10 comment says ">", but code actually checks "!=" //temp.Length != 0 BoundExpression lengthCheck = factory.Binary(BinaryOperatorKind.IntNotEqual, factory.SpecialType(SpecialType.System_Boolean), lengthCall, factory.Literal(0)); //((temp = array) != null && temp.Length != 0) BoundExpression condition = factory.Binary(BinaryOperatorKind.LogicalBoolAnd, factory.SpecialType(SpecialType.System_Boolean), notNullCheck, lengthCheck); //temp[0] BoundExpression firstElement = factory.ArrayAccessFirstElement(factory.Local(pinnedTemp)); // NOTE: this is a fixed statement address-of in that it's the initial value of the pointer. //&temp[0] BoundExpression firstElementAddress = new BoundAddressOfOperator(factory.Syntax, firstElement, type: new PointerTypeSymbol(arrayElementType)); BoundExpression convertedFirstElementAddress = ApplyConversionIfNotIdentity(fixedInitializer.ElementPointerConversion, fixedInitializer.ElementPointerPlaceholder, firstElementAddress); //loc = &temp[0] BoundExpression consequenceAssignment = factory.AssignmentExpression(factory.Local(localSymbol), convertedFirstElementAddress); //loc = null BoundExpression alternativeAssignment = factory.AssignmentExpression(factory.Local(localSymbol), factory.Null(localType)); //(((temp = array) != null && temp.Length != 0) ? loc = &temp[0] : loc = null) BoundStatement localInit = factory.ExpressionStatement( new BoundConditionalOperator(factory.Syntax, false, condition, consequenceAssignment, alternativeAssignment, ConstantValue.NotAvailable, localType, wasTargetTyped: false, localType)); return InstrumentLocalDeclarationIfNecessary(localDecl, localSymbol, localInit); } } }