|
// 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;
}
// .GetPinnable()
var getPinnableCall = getPinnableMethod.IsStatic ?
factory.Call(null, getPinnableMethod, callReceiver) :
factory.Call(callReceiver, 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);
}
}
}
|