/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Compilers/CSharp/Portable/CodeGen/Optimizer.cs
2 496 строк
92 KB
AlekseyTs
Fix crash in code gen (#84681)
30 июл 2026, 15:07
Не верифицирован
30 июл 2026, 15:07
677eb5a
Код
Авторство
О чём код?
// 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. #nullable disable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.CodeGen { internal class Optimizer { /// <summary> /// Perform IL specific optimizations (mostly reduction of local slots) /// </summary> /// <param name="src">Method body to optimize</param> /// <param name="debugFriendly"> /// When set, do not perform aggressive optimizations that degrade debugging experience. /// In particular we do not do the following: /// /// 1) Do not elide any user defined locals, even if never read from. /// Example: /// { /// var dummy = Goo(); // should not become just "Goo" /// } /// /// User might want to examine dummy in the debugger. /// /// 2) Do not carry values on the stack between statements /// Example: /// { /// var temp = Goo(); /// temp.ToString(); // should not become Goo().ToString(); /// } /// /// User might want to examine temp in the debugger. /// /// </param> /// <param name="stackLocals"> /// Produced list of "ephemeral" locals. /// Essentially, these locals do not need to leave the evaluation stack. /// As such they do not require an allocation of a local slot and /// their load/store operations are implemented trivially. /// </param> /// <returns></returns> public static BoundStatement Optimize( BoundStatement src, bool debugFriendly, out HashSet<LocalSymbol> stackLocals) { //TODO: run other optimizing passes here. // stack scheduler must be the last one. var locals = PooledDictionary<LocalSymbol, LocalDefUseInfo>.GetInstance(); src = (BoundStatement)StackOptimizerPass1.Analyze(src, locals, debugFriendly); FilterValidStackLocals(locals); BoundStatement result; if (locals.Count == 0) { stackLocals = null; result = src; } else { stackLocals = new HashSet<LocalSymbol>(locals.Keys); result = StackOptimizerPass2.Rewrite(src, locals); } foreach (var info in locals.Values) { info.Free(); } locals.Free(); return result; } private static void FilterValidStackLocals(Dictionary<LocalSymbol, LocalDefUseInfo> info) { // remove fake dummies and variable that cannot be scheduled var dummies = ArrayBuilder<LocalDefUseInfo>.GetInstance(); foreach (var local in info.Keys.ToArray()) { var locInfo = info[local]; if (local.SynthesizedKind == SynthesizedLocalKind.OptimizerTemp) { dummies.Add(locInfo); info.Remove(local); } else if (locInfo.CannotSchedule) { locInfo.Free(); info.Remove(local); } } if (info.Count != 0) { RemoveIntersectingLocals(info, dummies); } foreach (var dummy in dummies) { dummy.Free(); } dummies.Free(); } private static void RemoveIntersectingLocals(Dictionary<LocalSymbol, LocalDefUseInfo> info, ArrayBuilder<LocalDefUseInfo> dummies) { // Add dummy definitions. // Although we do not schedule dummies we intend to guarantee that no // local definition span intersects with definition spans of a dummy // that will ensure that at any access to dummy is done on same stack state. var defs = ArrayBuilder<LocalDefUseSpan>.GetInstance(dummies.Count); foreach (var dummy in dummies) { foreach (var def in dummy.LocalDefs) { // not interested in single node definitions if (def.Start != def.End) { defs.Add(def); } } } var dummyCnt = defs.Count; //TODO: perf. This can be simplified to not use a query. // order definitions by increasing size // this will give preference to shorter def-use spans when they intersect // // also order by usage, giving preference to spans at the beginning of the method var ordered = from i in info from d in i.Value.LocalDefs orderby d.End - d.Start, d.End ascending select new { i = i.Key, d = d }; // collect non-intersecting def-use spans. // if span intersects with something already stored, reject corresponding variable. // // CONSIDER: do we want to remove already added spans of rejected variables? // When I tried it did not improve results much. So I will keep it simple. foreach (var pair in ordered) { if (!info.ContainsKey(pair.i)) { // this pair belongs to a local that is already rejected // no need to waste time on it continue; } var newDef = pair.d; var cnt = defs.Count; bool intersects; // 5000 here is just a "sufficiently large number" // in practice cnt rarely exceeds 200 if (cnt > 5000) { // too many locals/spans. // This is an n^2 check and optimizing further may become costly. // reject all following definition spans intersects = true; } else { intersects = false; for (int i = 0; i < dummyCnt; i++) { var def = defs[i]; if (newDef.ConflictsWithDummy(def)) { intersects = true; break; } } if (!intersects) { for (int i = dummyCnt; i < cnt; i++) { var def = defs[i]; if (newDef.ConflictsWith(def)) { intersects = true; break; } } } } if (intersects) { info[pair.i].Free(); info.Remove(pair.i); } else { defs.Add(newDef); } } defs.Free(); } } // represents a local and its Def-Use-Use chain // // NOTE: stack local reads are destructive to the locals so // if the read is not the last one, it must be immediately followed by // another definition. // For the rewriting purposes it is irrelevant if definition was created by // a write or a subsequent read. These cases are not ambiguous because // when rewriting, definition will match to a single node and // we always know if given node is reading or writing. internal class LocalDefUseInfo { // stack at variable declaration, may be > 0 in sequences. public int StackAtDeclaration { get; private set; } private readonly ObjectPool<LocalDefUseInfo> _pool; // global pool private static readonly ObjectPool<LocalDefUseInfo> s_poolInstance = CreatePool(); // value definitions for this variable. private ArrayBuilder<LocalDefUseSpan> _localDefs; public ArrayBuilder<LocalDefUseSpan> LocalDefs { get { var result = _localDefs; if (result == null) { _localDefs = result = ArrayBuilder<LocalDefUseSpan>.GetInstance(); } return result; } } // once this goes to true we are no longer interested in this variable. public bool CannotSchedule { get; private set; } public void ShouldNotSchedule() { this.CannotSchedule = true; } private LocalDefUseInfo(ObjectPool<LocalDefUseInfo> pool) { _pool = pool; } public void Free() { if (_localDefs != null) { _localDefs.Free(); _localDefs = null; } _pool?.Free(this); } // if someone needs to create a pool; public static ObjectPool<LocalDefUseInfo> CreatePool() { ObjectPool<LocalDefUseInfo> pool = null; pool = new ObjectPool<LocalDefUseInfo>(() => new LocalDefUseInfo(pool), 128); return pool; } public static LocalDefUseInfo GetInstance(int stackAtDeclaration) { var instance = s_poolInstance.Allocate(); Debug.Assert(instance._localDefs == null); instance.StackAtDeclaration = stackAtDeclaration; instance.CannotSchedule = false; return instance; } } // represents a span of a value between definition and use. // start/end positions are specified in terms of global node count as visited by // StackOptimizer visitors. (i.e. recursive walk not looking into constants) internal readonly struct LocalDefUseSpan { public readonly int Start; public readonly int End; public LocalDefUseSpan(int start) : this(start, start) { } private LocalDefUseSpan(int start, int end) { this.Start = start; this.End = end; } internal LocalDefUseSpan WithEnd(int end) { return new LocalDefUseSpan(this.Start, end); } public override string ToString() { return "[" + this.Start + " ," + this.End + ")"; } /// <summary> /// when current and other use spans are regular spans we can have only 2 conflict cases: /// [1, 3) conflicts with [0, 2) /// [1, 3) conflicts with [2, 4) /// /// NOTE: with regular spans, it is not possible for two spans to share an edge point /// unless they belong to the same local. (because we cannot access two real locals at the same time) /// /// specifically: /// [1, 3) does not conflict with [0, 1) since such spans would need to belong to the same local /// </summary> public bool ConflictsWith(LocalDefUseSpan other) { return Contains(other.Start) ^ Contains(other.End); } private bool Contains(int val) { return this.Start < val && this.End > val; } /// <summary> /// Dummy locals represent implicit control flow /// It is not allowed for a regular local span to cross into or /// be immediately adjacent to a dummy span. /// /// specifically: /// [1, 3) does conflict with [0, 1) since that would imply a value flowing into or out of a span surrounded by a branch/label /// /// </summary> public bool ConflictsWithDummy(LocalDefUseSpan dummy) { return Includes(dummy.Start) ^ Includes(dummy.End); } private bool Includes(int val) { return this.Start <= val && this.End >= val; } } // context of expression evaluation. // it will affect inference of stack behavior // it will also affect when locals can be scheduled to the stack // Example: // Goo(x, ref x) <-- x cannot be a stack local as it is used in different contexts. internal enum ExprContext { None, Sideeffects, Value, Address, AssignmentTarget, Box } // Analyzes the tree trying to figure which locals may live on stack. // It is a fairly delicate process and must be very familiar with how CodeGen works. // It is essentially a part of CodeGen. // // NOTE: It is always safe to mark a local as not eligible as a stack local // so when situation gets complicated we just refuse to schedule and move on. // internal sealed class StackOptimizerPass1 : BoundTreeRewriter { private readonly bool _debugFriendly; private readonly ArrayBuilder<(BoundExpression, ExprContext)> _evalStack; private int _counter; private ExprContext _context; private BoundLocal _assignmentLocal; private readonly Dictionary<LocalSymbol, LocalDefUseInfo> _locals; // we need to guarantee same stack patterns at branches and labels. // we do that by placing a fake dummy local at one end of a branch and force that it is accessible at another. // if any stack local tries to intervene and misbalance the stack, it will clash with the dummy and will be rejected. private readonly SmallDictionary<object, DummyLocal> _dummyVariables = new SmallDictionary<object, DummyLocal>(ReferenceEqualityComparer.Instance); // fake local that represents the eval stack. // when we need to ensure that eval stack is not blocked by stack Locals, we record an access to empty. public static readonly DummyLocal empty = new DummyLocal(); private int _recursionDepth; private StackOptimizerPass1(Dictionary<LocalSymbol, LocalDefUseInfo> locals, ArrayBuilder<ValueTuple<BoundExpression, ExprContext>> evalStack, bool debugFriendly) { _locals = locals; _evalStack = evalStack; _debugFriendly = debugFriendly; // this is the top of eval stack DeclareLocal(empty, 0); RecordDummyWrite(empty); } public static BoundNode Analyze( BoundNode node, Dictionary<LocalSymbol, LocalDefUseInfo> locals, bool debugFriendly) { var evalStack = ArrayBuilder<ValueTuple<BoundExpression, ExprContext>>.GetInstance(); var analyzer = new StackOptimizerPass1(locals, evalStack, debugFriendly); var rewritten = analyzer.Visit(node); evalStack.Free(); return rewritten; } public override BoundNode Visit(BoundNode node) { BoundNode result; BoundExpression expr = node as BoundExpression; if (expr != null) { Debug.Assert(expr.Kind != BoundKind.Label); result = VisitExpression(expr, ExprContext.Value); } else { result = VisitStatement(node); } return result; } private BoundExpression VisitExpressionCore(BoundExpression node, ExprContext context) { var prevContext = _context; int prevStack = StackDepth(); _context = context; // Do not recurse into constant expressions. Their children do not push any values. var result = node.ConstantValueOpt == null ? node = (BoundExpression)base.Visit(node) : node; _context = prevContext; _counter += 1; switch (context) { case ExprContext.Sideeffects: SetStackDepth(prevStack); break; case ExprContext.AssignmentTarget: break; case ExprContext.Value: case ExprContext.Address: case ExprContext.Box: SetStackDepth(prevStack); PushEvalStack(node, context); break; default: throw ExceptionUtilities.UnexpectedValue(context); } return result; } private BoundExpression VisitExpression(BoundExpression node, ExprContext context) { BoundExpression result; _recursionDepth++; if (_recursionDepth > 1) { StackGuard.EnsureSufficientExecutionStack(_recursionDepth); result = VisitExpressionCore(node, context); } else { result = VisitExpressionCoreWithStackGuard(node, context); } _recursionDepth--; return result; } private BoundExpression VisitExpressionCoreWithStackGuard(BoundExpression node, ExprContext context) { Debug.Assert(_recursionDepth == 1); try { var result = VisitExpressionCore(node, context); Debug.Assert(_recursionDepth == 1); return result; } catch (InsufficientExecutionStackException ex) { throw new CancelledByStackGuardException(ex, node); } } protected override BoundNode VisitExpressionOrPatternWithoutStackGuard(BoundNode node) { throw ExceptionUtilities.Unreachable(); } private void PushEvalStack(BoundExpression result, ExprContext context) { Debug.Assert(result != null || context == ExprContext.None); _evalStack.Add((result, context)); } private int StackDepth() { return _evalStack.Count; } private bool EvalStackIsEmpty() { return StackDepth() == 0; } private void SetStackDepth(int depth) { _evalStack.Clip(depth); } private void PopEvalStack() { SetStackDepth(_evalStack.Count - 1); } public BoundNode VisitStatement(BoundNode node) { Debug.Assert(node == null || EvalStackIsEmpty()); return VisitSideEffect(node); } public BoundNode VisitSideEffect(BoundNode node) { var origStack = StackDepth(); var prevContext = _context; var result = base.Visit(node); // prevent cross-statement local optimizations // when emitting debug-friendly code. if (_debugFriendly) { EnsureOnlyEvalStack(); } _context = prevContext; SetStackDepth(origStack); _counter += 1; return result; } public override BoundNode VisitConversion(BoundConversion node) { var context = _context == ExprContext.Sideeffects && !node.ConversionHasSideEffects() ? ExprContext.Sideeffects : ExprContext.Value; return node.UpdateOperand(this.VisitExpression(node.Operand, context)); } public override BoundNode VisitPassByCopy(BoundPassByCopy node) { var context = _context == ExprContext.Sideeffects ? ExprContext.Sideeffects : ExprContext.Value; return node.Update( this.VisitExpression(node.Expression, context), node.Type); } public override BoundNode VisitBlock(BoundBlock node) { Debug.Assert(EvalStackIsEmpty(), "entering blocks when evaluation stack is not empty?"); if (node.Instrumentation != null) { foreach (var local in node.Instrumentation.Locals) DeclareLocal(local, stack: 0); } // normally we would not allow stack locals // when evaluation stack is not empty. DeclareLocals(node.Locals, stack: 0); return base.VisitBlock(node); } // here we have a case of indirect assignment: *t1 = expr; // normally we would need to push t1 and that will cause spilling of t2 // // TODO: an interesting case arises in unused x[i]++ and ++x[i] : // we have trees that look like: // // t1 = &(x[0]) // t2 = *t1 // *t1 = t2 + 1 // // t1 = &(x[0]) // t2 = *t1 + 1 // *t1 = t2 // // in these cases, we could keep t2 on stack (dev10 does). // we are dealing with exactly 2 locals and access them in strict order // t1, t2, t1, t2 and we are not using t2 after that. // We may consider detecting exactly these cases and pretend that we do not need // to push either t1 or t2 in this case. // public override BoundNode VisitSequence(BoundSequence node) { // Normally we can only use stack for local scheduling if stack is not used for evaluation. // In a context of a regular block that simply means that eval stack must be empty. // Sequences, can be entered on a nonempty evaluation stack // Ex: // a.b = Seq{var y, y = 1, y} // a is on the stack for the duration of the sequence. // // However evaluation stack at the entry cannot be used inside the sequence, so such stack // works as effective "empty" for locals declared in sequence. // Therefore sequence locals can be stack scheduled at same stack as at the entry to the sequence. // it may seem attractive to relax the stack requirement to be: // "all uses must agree on stack depth". // The following example illustrates a case where x is safely used at "declarationStack + 1" // Ex: // Seq{var x; y.a = Seq{x = 1; x}; y} // x is used while y is on the eval stack // // It is, however not safe assumption in general since eval stack may be accessed between usages. // Ex: // Seq{var x; y.a = Seq{x = 1; x}; y.z = x; y} // x blocks access to y // // There is one case where we want to tweak the "use at declaration stack" rule - in the case of // compound assignment that involves ByRef operand captures (like: x[y]++ ) . // // Those cases produce specific sequences of the shapes: // // prefix: Seq{var temp, ref operand; operand initializers; *operand = Seq{temp = (T)(operand + 1); temp;} result: temp} // postfix: Seq{var temp, ref operand; operand initializers; *operand = Seq{temp = operand; ; (T)(temp + 1);} result: temp} // // 1) temp is used as the result of the sequence (and that is the only reason why it is declared in the outer sequence). // 2) all side-effects except the last one do not use the temp. // 3) last side-effect is an indirect assignment of a sequence (and target does not involve the temp). // // Note that in a case of side-effects context, the result value will be ignored and therefore // all usages of the nested temp will be confined to the nested sequence that is executed at +1 stack. // // We will detect such case and indicate +1 as the desired stack depth at local accesses. // var declarationStack = StackDepth(); var locals = node.Locals; if (!locals.IsDefaultOrEmpty) { if (_context == ExprContext.Sideeffects) { foreach (var local in locals) { if (IsNestedLocalOfCompoundOperator(local, node)) { // special case DeclareLocal(local, declarationStack + 1); } else { DeclareLocal(local, declarationStack); } } } else { DeclareLocals(locals, declarationStack); } } // rewrite operands var origContext = _context; var sideeffects = node.SideEffects; ArrayBuilder<BoundExpression> rewrittenSideeffects = null; if (!sideeffects.IsDefault) { for (int i = 0; i < sideeffects.Length; i++) { var sideeffect = sideeffects[i]; var rewrittenSideeffect = this.VisitExpression(sideeffect, ExprContext.Sideeffects); if (rewrittenSideeffects == null && rewrittenSideeffect != sideeffect) { rewrittenSideeffects = ArrayBuilder<BoundExpression>.GetInstance(); rewrittenSideeffects.AddRange(sideeffects, i); } if (rewrittenSideeffects != null) { rewrittenSideeffects.Add(rewrittenSideeffect); } } } var value = this.VisitExpression(node.Value, origContext); return node.Update(node.Locals, rewrittenSideeffects != null ? rewrittenSideeffects.ToImmutableAndFree() : sideeffects, value, node.Type); } // detect a pattern used in compound operators // where a temp is declared in the outer sequence // only because it must be returned, otherwise all uses are // confined to the nested sequence that is assigned indirectly of to an instance field (and therefore has +1 stack) // in such case the desired stack for this local is +1 private bool IsNestedLocalOfCompoundOperator(LocalSymbol local, BoundSequence node) { var value = node.Value; // local must be used as the value of the sequence. if (value != null && value.Kind == BoundKind.Local && ((BoundLocal)value).LocalSymbol == local) { var sideeffects = node.SideEffects; var lastSideeffect = sideeffects.LastOrDefault(); if (lastSideeffect != null) { // last side-effect must be an indirect assignment of a sequence. if (lastSideeffect.Kind == BoundKind.AssignmentOperator) { var assignment = (BoundAssignmentOperator)lastSideeffect; if (IsIndirectOrInstanceFieldAssignment(assignment) && assignment.Right.Kind == BoundKind.Sequence) { // and no other side-effects should use the variable var localUsedWalker = new LocalUsedWalker(local, _recursionDepth); for (int i = 0; i < sideeffects.Length - 1; i++) { if (localUsedWalker.IsLocalUsedIn(sideeffects[i])) { return false; } } // and local is not used on the left of the assignment // (extra check, but better be safe) if (localUsedWalker.IsLocalUsedIn(assignment.Left)) { return false; } // it should be used somewhere Debug.Assert(localUsedWalker.IsLocalUsedIn(assignment.Right), "who assigns the temp?"); return true; } } } } return false; } private sealed class LocalUsedWalker : BoundTreeWalkerWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator { private readonly LocalSymbol _local; private bool _found; internal LocalUsedWalker(LocalSymbol local, int recursionDepth) : base(recursionDepth) { _local = local; } public bool IsLocalUsedIn(BoundNode node) { _found = false; this.Visit(node); return _found; } public override BoundNode Visit(BoundNode node) { if (!_found) { return base.Visit(node); } return null; } public override BoundNode VisitLocal(BoundLocal node) { if (node.LocalSymbol == _local) { _found = true; } return null; } } public override BoundNode VisitExpressionStatement(BoundExpressionStatement node) { return node.Update( this.VisitExpression(node.Expression, ExprContext.Sideeffects)); } public override BoundNode VisitLocal(BoundLocal node) { if (node.ConstantValueOpt == null) { switch (_context) { case ExprContext.Address: if (node.LocalSymbol.RefKind != RefKind.None) { RecordVarRead(node.LocalSymbol); } else { RecordVarRef(node.LocalSymbol); } break; case ExprContext.AssignmentTarget: Debug.Assert(_assignmentLocal == null); // actual assignment will happen later, after Right is evaluated // just remember what we are assigning to. _assignmentLocal = node; break; case ExprContext.Sideeffects: if (node.LocalSymbol.RefKind != RefKind.None) { // Reading from a ref has a side effect since the read // may result in a NullReferenceException. RecordVarRead(node.LocalSymbol); } break; case ExprContext.Value: case ExprContext.Box: RecordVarRead(node.LocalSymbol); break; } } return base.VisitLocal(node); } public override BoundNode VisitAssignmentOperator(BoundAssignmentOperator node) { var sequence = node.Left as BoundSequence; if (sequence != null) { // assigning to a sequence is uncommon, but could happen in a // case if LHS was a declaration expression. // // Just rewrite {se1, se2, se3, val} = something // into ==> {se1, se2, se3, val = something} BoundExpression rewritten = sequence.Update(sequence.Locals, sequence.SideEffects, node.Update(sequence.Value, node.Right, node.IsRef, node.Type), sequence.Type); rewritten = (BoundExpression)Visit(rewritten); // do not count the assignment twice _counter--; return rewritten; } var isIndirectAssignment = IsIndirectAssignment(node); var left = VisitExpression(node.Left, isIndirectAssignment ? ExprContext.Address : ExprContext.AssignmentTarget); // must delay recording a write until after RHS is evaluated var assignmentLocal = _assignmentLocal; _assignmentLocal = null; Debug.Assert(_context != ExprContext.AssignmentTarget, "assignment expression cannot be a target of another assignment"); ExprContext rhsContext; if (node.IsRef || _context == ExprContext.Address) { // we need the address of rhs one way or another so we cannot have it on the stack. rhsContext = ExprContext.Address; } else { Debug.Assert(_context == ExprContext.Value || _context == ExprContext.Box || _context == ExprContext.Sideeffects, "assignment expression cannot be a target of another assignment"); // we only need a value of rhs, so if otherwise possible it can be a stack value. rhsContext = ExprContext.Value; } // if right is a struct ctor, it may be optimized into in-place call // Such call will push the receiver ref before the arguments // so we need to ensure that arguments cannot use stack temps BoundExpression right = node.Right; bool mayPushReceiver = (right.Kind == BoundKind.ObjectCreationExpression && right.Type.IsVerifierValue() && ((BoundObjectCreationExpression)right).Constructor.ParameterCount != 0); if (mayPushReceiver) { // push unknown value just to prevent access to stack locals. PushEvalStack(null, ExprContext.None); } right = VisitExpression(node.Right, rhsContext); if (mayPushReceiver) { PopEvalStack(); } // if assigning to a local, now it is the time to record the Write if (assignmentLocal != null) { // This assert will fire if code relies on implicit CLR coercions // - i.e assigns int value to a short local. // in that case we should force lhs to be a real local. Debug.Assert( node.Left.Type.Equals(node.Right.Type, TypeCompareKind.AllIgnoreOptions) || IsFixedBufferAssignmentToRefLocal(node.Left, node.Right, node.IsRef), @"type of the assignment value is not the same as the type of assignment target. This is not expected by the optimizer and is typically a result of a bug somewhere else."); Debug.Assert(!isIndirectAssignment, "indirect assignment is a read, not a write"); LocalSymbol localSymbol = assignmentLocal.LocalSymbol; // If the LHS is a readonly ref and the result is used later we cannot stack // schedule since we may be converting a writeable value on the RHS to a readonly // one on the LHS. if (localSymbol.RefKind is RefKind.RefReadOnly or RefKindExtensions.StrictIn && (_context == ExprContext.Address || _context == ExprContext.Value)) { ShouldNotSchedule(localSymbol); } // Special Case: If the RHS is a pointer conversion, then the assignment functions as // a conversion (because the RHS will actually be typed as a native u/int in IL), so // we should not optimize away the local (i.e. schedule it on the stack). else if (CanScheduleToStack(localSymbol) && assignmentLocal.Type.IsPointerOrFunctionPointer() && right.Kind == BoundKind.Conversion && ((BoundConversion)right).ConversionKind.IsPointerConversion()) { ShouldNotSchedule(localSymbol); } // If this is a pointer-to-ref assignment, keep the local so GC knows to re-track it. // We don't need to do this for implicitly synthesized locals because working with pointers in an unsafe context does not guarantee any GC tracking, // but when a pointer is converted to a user-defined ref local, it becomes a use of a "safe" feature where we should guarantee the ref is tracked by GC. else if (localSymbol.RefKind != RefKind.None && localSymbol.SynthesizedKind == SynthesizedLocalKind.UserDefined && PointerIndirectionMayFlowToRefResultVisitor.Check(right, _recursionDepth)) { ShouldNotSchedule(localSymbol); } RecordVarWrite(localSymbol); assignmentLocal = null; } return node.Update(left, right, node.IsRef, node.Type); } /// <summary> /// Fixed-sized buffers are lowered as field accesses with pointer type, but /// we want to assign them to pinned ref locals before creating the pointer /// type, so this results in an assignment with mismatched types (pointer to managed /// ref). This is legal according to the CLR, but not how we usually represent things /// in lowering. /// </summary> internal static bool IsFixedBufferAssignmentToRefLocal(BoundExpression left, BoundExpression right, bool isRef) => isRef && right is BoundFieldAccess fieldAccess && fieldAccess.FieldSymbol.IsFixedSizeBuffer && left.Type.Equals(((PointerTypeSymbol)right.Type).PointedAtType, TypeCompareKind.AllIgnoreOptions); // indirect assignment is assignment to a value referenced indirectly // it may only happen if // 1) lhs is a reference (must be a parameter or a local) // 2) it is not a ref/out assignment where the reference itself would be assigned private static bool IsIndirectAssignment(BoundAssignmentOperator node) { var lhs = node.Left; Debug.Assert(!node.IsRef || (lhs.Kind is BoundKind.Local or BoundKind.Parameter or BoundKind.FieldAccess && lhs.GetRefKind() != RefKind.None), "only ref symbols can be a target of a ref assignment"); switch (lhs.Kind) { case BoundKind.ThisReference: Debug.Assert(lhs.Type.IsValueType, "'this' is assignable only in structs"); return true; case BoundKind.Parameter: if (((BoundParameter)lhs).ParameterSymbol.RefKind != RefKind.None) { return !node.IsRef; } return false; case BoundKind.Local: if (((BoundLocal)lhs).LocalSymbol.RefKind != RefKind.None) { return !node.IsRef; } return false; case BoundKind.Call: Debug.Assert(((BoundCall)lhs).Method.RefKind == RefKind.Ref, "only ref returning methods are assignable"); return true; case BoundKind.FunctionPointerInvocation: Debug.Assert(((BoundFunctionPointerInvocation)lhs).FunctionPointer.Signature.RefKind == RefKind.Ref, "only ref returning function pointers are assignable"); return true; case BoundKind.ConditionalOperator: Debug.Assert(((BoundConditionalOperator)lhs).IsRef, "only ref ternaries are assignable"); return true; case BoundKind.AssignmentOperator: Debug.Assert(((BoundAssignmentOperator)lhs).IsRef, "only ref assignments are assignable"); return true; case BoundKind.Sequence: // The assignment should have been pushed inside the sequence during lowering, // so instead of `{...side-effects..., val} = something` we should get here with `{...side-effects..., (val = something)}`. Debug.Assert(!IsIndirectAssignment(node.Update(((BoundSequence)node.Left).Value, node.Right, node.IsRef, node.Type)), "indirect assignment to a sequence is unexpected"); return false; case BoundKind.RefValueOperator: case BoundKind.PointerIndirectionOperator: case BoundKind.PseudoVariable: return true; case BoundKind.ModuleVersionId: case BoundKind.InstrumentationPayloadRoot: // these are just stores into special static fields goto case BoundKind.FieldAccess; case BoundKind.FieldAccess: case BoundKind.ArrayAccess: // always symbolic stores return false; default: throw ExceptionUtilities.UnexpectedValue(lhs.Kind); } } private static bool IsIndirectOrInstanceFieldAssignment(BoundAssignmentOperator node) { var lhs = node.Left; if (lhs.Kind == BoundKind.FieldAccess) { return !((BoundFieldAccess)lhs).FieldSymbol.IsStatic; } return IsIndirectAssignment(node); } public override BoundNode VisitCall(BoundCall node) { if (node.ReceiverOpt is BoundCall receiver) { int prevStack = StackDepth(); var calls = ArrayBuilder<BoundCall>.GetInstance(); calls.Push(node); node = receiver; while (node.ReceiverOpt is BoundCall receiver2) { calls.Push(node); node = receiver2; } BoundExpression rewrittenReceiver = visitReceiver(node); while (true) { rewrittenReceiver = visitArgumentsAndUpdateCall(node, rewrittenReceiver); receiver = node; if (!calls.TryPop(out node)) { break; } CheckCallReceiver(receiver, node); // VisitCallOrConditionalAccessReceiver does this // VisitExpressionCore does this after visiting each node _counter++; SetStackDepth(prevStack); PushEvalStack(receiver, GetReceiverContext(receiver)); } calls.Free(); return rewrittenReceiver; } else { BoundExpression rewrittenReceiver = visitReceiver(node); return visitArgumentsAndUpdateCall(node, rewrittenReceiver); } BoundExpression visitReceiver(BoundCall node) { var receiver = node.ReceiverOpt; MethodSymbol method = node.Method; // matches or a bit stronger than EmitReceiverRef // if there are any doubts that receiver is a ref type, // assume we will need an address (that will prevent scheduling of receiver). if (method.RequiresInstanceReceiver) { receiver = VisitCallOrConditionalAccessReceiver(receiver, node); } else { Debug.Assert(method.IsStatic); _counter += 1; if ((method.IsAbstract || method.IsVirtual) && receiver is BoundTypeExpression { Type: TypeParameterSymbol or NamedTypeSymbol { IsUnionType: true } } typeExpression) { receiver = typeExpression.Update(aliasOpt: null, boundContainingTypeOpt: null, boundDimensionsOpt: ImmutableArray<BoundExpression>.Empty, typeWithAnnotations: typeExpression.TypeWithAnnotations, type: this.VisitType(typeExpression.Type)); } else { receiver = null; } } return receiver; } BoundCall visitArgumentsAndUpdateCall(BoundCall node, BoundExpression receiver) { var rewrittenArguments = VisitArguments(node.Arguments, node.Method.Parameters, node.ArgumentRefKindsOpt); return node.Update(receiver, initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown, node.Method, rewrittenArguments); } } private BoundExpression VisitCallOrConditionalAccessReceiver(BoundExpression receiver, BoundCall callOpt) { var receiverType = receiver.Type; if (callOpt is { } call) { CheckCallReceiver(receiver, call); } ExprContext context = GetReceiverContext(receiver); receiver = VisitExpression(receiver, context); return receiver; } private void CheckCallReceiver(BoundExpression receiver, BoundCall call) { if (CodeGenerator.IsRef(receiver) && CodeGenerator.IsPossibleReferenceTypeReceiverOfConstrainedCall(receiver) && !CodeGenerator.IsSafeToDereferenceReceiverRefAfterEvaluatingArguments(call.Arguments)) { var unwrappedSequence = receiver; while (unwrappedSequence is BoundSequence sequence) { unwrappedSequence = sequence.Value; } if (unwrappedSequence is BoundLocal { LocalSymbol: { RefKind: not RefKind.None } localSymbol }) { ShouldNotSchedule(localSymbol); // Otherwise CodeGenerator is unable to apply proper fixups } } } private static ExprContext GetReceiverContext(BoundExpression receiver) { var receiverType = receiver.Type; ExprContext context; if (receiverType.IsReferenceType) { if (receiverType.IsTypeParameter()) { // type param receiver that we statically know is a reference will be boxed context = ExprContext.Box; } else { // reference receivers will be used as values context = ExprContext.Value; } } else { // everything else will get an address taken context = ExprContext.Address; } return context; } private ImmutableArray<BoundExpression> VisitArguments(ImmutableArray<BoundExpression> arguments, ImmutableArray<ParameterSymbol> parameters, ImmutableArray<RefKind> argRefKindsOpt) { Debug.Assert(!arguments.IsDefault); Debug.Assert(!parameters.IsDefault); // If this is a varargs method then there will be one additional argument for the __arglist(). Debug.Assert(arguments.Length == parameters.Length || arguments.Length == parameters.Length + 1); ArrayBuilder<BoundExpression> rewrittenArguments = null; for (int i = 0; i < arguments.Length; i++) { RefKind argRefKind = CodeGenerator.GetArgumentRefKind(arguments, parameters, argRefKindsOpt, i); VisitArgument(arguments, ref rewrittenArguments, i, argRefKind); } return rewrittenArguments != null ? rewrittenArguments.ToImmutableAndFree() : arguments; } private void VisitArgument(ImmutableArray<BoundExpression> arguments, ref ArrayBuilder<BoundExpression> rewrittenArguments, int i, RefKind argRefKind) { ExprContext context = (argRefKind == RefKind.None) ? ExprContext.Value : ExprContext.Address; var arg = arguments[i]; BoundExpression rewrittenArg = VisitExpression(arg, context); if (rewrittenArguments == null && arg != rewrittenArg) { rewrittenArguments = ArrayBuilder<BoundExpression>.GetInstance(); rewrittenArguments.AddRange(arguments, i); } if (rewrittenArguments != null) { rewrittenArguments.Add(rewrittenArg); } } public override BoundNode VisitArgListOperator(BoundArgListOperator node) { ArrayBuilder<BoundExpression> rewrittenArguments = null; ImmutableArray<BoundExpression> arguments = node.Arguments; ImmutableArray<RefKind> argRefKindsOpt = node.ArgumentRefKindsOpt; for (int i = 0; i < arguments.Length; i++) { RefKind refKind = argRefKindsOpt.IsDefaultOrEmpty ? RefKind.None : argRefKindsOpt[i]; VisitArgument(arguments, ref rewrittenArguments, i, refKind); } return node.Update(rewrittenArguments?.ToImmutableAndFree() ?? arguments, argRefKindsOpt, node.Type); } public override BoundNode VisitMakeRefOperator(BoundMakeRefOperator node) { // The __makeref(x) operator is logically like calling a method // static TypedReference MakeReference(ref T x) var rewrittenOperand = VisitExpression(node.Operand, ExprContext.Address); return node.Update(rewrittenOperand, node.Type); } public override BoundNode VisitObjectCreationExpression(BoundObjectCreationExpression node) { var constructor = node.Constructor; var rewrittenArguments = VisitArguments(node.Arguments, constructor.Parameters, node.ArgumentRefKindsOpt); Debug.Assert(node.InitializerExpressionOpt == null); return node.Update(constructor, rewrittenArguments, node.ArgumentNamesOpt, node.ArgumentRefKindsOpt, node.Expanded, node.ArgsToParamsOpt, node.DefaultArguments, node.ConstantValueOpt, initializerExpressionOpt: null, node.Type); } public override BoundNode VisitArrayAccess(BoundArrayAccess node) { // regardless of purpose, array access visits its children as values // TODO: do we need to save/restore old context here? var oldContext = _context; _context = ExprContext.Value; var result = base.VisitArrayAccess(node); _context = oldContext; return result; } public override BoundNode VisitFieldAccess(BoundFieldAccess node) { var field = node.FieldSymbol; var receiver = node.ReceiverOpt; // if there are any doubts that receiver is a ref type, // assume we will need an address. (that will prevent scheduling of receiver). if (!field.IsStatic) { if (receiver.Type.IsTypeParameter()) { // type parameters must be boxed to access fields. receiver = VisitExpression(receiver, ExprContext.Box); } else { // need address when assigning to a field and receiver is not a reference // when accessing a field of a struct unless we only need Value and Value is preferred. if (receiver.Type.IsValueType && ( _context == ExprContext.AssignmentTarget || _context == ExprContext.Address || CodeGenerator.FieldLoadMustUseRef(receiver))) { receiver = VisitExpression(receiver, ExprContext.Address); } else { receiver = VisitExpression(receiver, ExprContext.Value); } } } else { // for some reason it could be not null even if field is static... // it seems wrong _counter += 1; receiver = null; } return node.Update(receiver, field, node.ConstantValueOpt, node.ResultKind, node.Type); } public override BoundNode VisitLabelStatement(BoundLabelStatement node) { RecordLabel(node.Label); return base.VisitLabelStatement(node); } public override BoundNode VisitLabel(BoundLabel node) { Debug.Assert(false, "we should not have label expressions at this stage"); return node; } public override BoundNode VisitIsPatternExpression(BoundIsPatternExpression node) { Debug.Assert(false, "we should not have is-pattern expressions at this stage"); return node; } public override BoundNode VisitGotoStatement(BoundGotoStatement node) { Debug.Assert(node.CaseExpressionOpt == null, "we should not have label expressions at this stage"); var result = base.VisitGotoStatement(node); RecordBranch(node.Label); return result; } public override BoundNode VisitConditionalGoto(BoundConditionalGoto node) { var result = base.VisitConditionalGoto(node); PopEvalStack(); // condition gets consumed. RecordBranch(node.Label); return result; } public override BoundNode VisitSwitchDispatch(BoundSwitchDispatch node) { Debug.Assert(EvalStackIsEmpty()); // switch dispatch needs a byval local or a parameter as a key. // if this is already a fitting local, let's keep it that way BoundExpression boundExpression = node.Expression; if (boundExpression.Kind == BoundKind.Local) { var localSym = ((BoundLocal)boundExpression).LocalSymbol; if (localSym.RefKind == RefKind.None) { ShouldNotSchedule(localSym); } } boundExpression = (BoundExpression)this.Visit(boundExpression); // expression value is consumed by the switch PopEvalStack(); // implicit control flow EnsureOnlyEvalStack(); RecordBranch(node.DefaultLabel); foreach ((_, LabelSymbol label) in node.Cases) { RecordBranch(label); } return node.Update(boundExpression, node.Cases, node.DefaultLabel, node.LengthBasedStringSwitchDataOpt); } public override BoundNode VisitConditionalOperator(BoundConditionalOperator node) { var origStack = StackDepth(); BoundExpression condition = this.VisitExpression(node.Condition, ExprContext.Value); var cookie = GetStackStateCookie(); // implicit goto here var context = node.IsRef ? ExprContext.Address : ExprContext.Value; SetStackDepth(origStack); // consequence is evaluated with original stack BoundExpression consequence = this.VisitExpression(node.Consequence, context); EnsureStackState(cookie); // implicit label here SetStackDepth(origStack); // alternative is evaluated with original stack BoundExpression alternative = this.VisitExpression(node.Alternative, context); EnsureStackState(cookie); // implicit label here return node.Update(node.IsRef, condition, consequence, alternative, node.ConstantValueOpt, node.NaturalTypeOpt, node.WasCompilerGenerated, node.Type); } public override BoundNode VisitBinaryOperator(BoundBinaryOperator node) { BoundExpression child = node.Left; if (child.Kind != BoundKind.BinaryOperator || child.ConstantValueOpt != null) { return VisitBinaryOperatorSimple(node); } // Do not blow the stack due to a deep recursion on the left. var stack = ArrayBuilder<BoundBinaryOperator>.GetInstance(); stack.Push(node); BoundBinaryOperator binary = (BoundBinaryOperator)child; while (true) { stack.Push(binary); child = binary.Left; if (child.Kind != BoundKind.BinaryOperator || child.ConstantValueOpt != null) { break; } binary = (BoundBinaryOperator)child; } var prevContext = _context; int prevStack = StackDepth(); var left = (BoundExpression)this.Visit(child); while (true) { binary = stack.Pop(); var isLogical = (binary.OperatorKind & BinaryOperatorKind.Logical) != 0; object cookie = null; if (isLogical) { cookie = GetStackStateCookie(); // implicit branch here SetStackDepth(prevStack); // right is evaluated with original stack } var right = (BoundExpression)this.Visit(binary.Right); if (isLogical) { EnsureStackState(cookie); // implicit label here } var type = this.VisitType(binary.Type); Debug.Assert(!binary.OperatorKind.IsDynamic()); left = binary.Update(binary.OperatorKind, binary.ConstantValueOpt, binary.BinaryOperatorMethod, binary.ConstrainedToType, binary.ResultKind, left, right, type); if (stack.Count == 0) { break; } _context = prevContext; _counter += 1; SetStackDepth(prevStack); PushEvalStack(binary, ExprContext.Value); } Debug.Assert((object)binary == node); stack.Free(); return left; } private BoundNode VisitBinaryOperatorSimple(BoundBinaryOperator node) { var isLogical = (node.OperatorKind & BinaryOperatorKind.Logical) != 0; if (isLogical) { var origStack = StackDepth(); BoundExpression left = (BoundExpression)this.Visit(node.Left); var cookie = GetStackStateCookie(); // implicit branch here SetStackDepth(origStack); // right is evaluated with original stack BoundExpression right = (BoundExpression)this.Visit(node.Right); EnsureStackState(cookie); // implicit label here Debug.Assert(!node.OperatorKind.IsDynamic()); return node.Update(node.OperatorKind, node.ConstantValueOpt, node.BinaryOperatorMethod, node.ConstrainedToType, node.ResultKind, left, right, node.Type); } return base.VisitBinaryOperator(node); } public override BoundNode VisitNullCoalescingOperator(BoundNullCoalescingOperator node) { Debug.Assert(node.LeftPlaceholder is null); Debug.Assert(node.LeftConversion is null); var origStack = StackDepth(); BoundExpression left = (BoundExpression)this.Visit(node.LeftOperand); var cookie = GetStackStateCookie(); // implicit branch here // right is evaluated with original stack // (this is not entirely true, codegen may keep left on the stack as an ephemeral temp, but that is irrelevant here) SetStackDepth(origStack); BoundExpression right = (BoundExpression)this.Visit(node.RightOperand); EnsureStackState(cookie); // implicit label here return node.Update(left, right, node.LeftPlaceholder, node.LeftConversion, node.OperatorResultKind, @checked: node.Checked, node.Type); } public override BoundNode VisitLoweredConditionalAccess(BoundLoweredConditionalAccess node) { var origStack = StackDepth(); BoundExpression receiver = VisitCallOrConditionalAccessReceiver(node.Receiver, callOpt: null); var cookie = GetStackStateCookie(); // implicit branch here // right is evaluated with original stack // (this is not entirely true, codegen will keep receiver on the stack, but that is irrelevant here) SetStackDepth(origStack); BoundExpression whenNotNull = (BoundExpression)this.Visit(node.WhenNotNull); EnsureStackState(cookie); // implicit label here var whenNull = node.WhenNullOpt; if (whenNull != null) { SetStackDepth(origStack); // whenNull is evaluated with original stack whenNull = (BoundExpression)this.Visit(whenNull); EnsureStackState(cookie); // implicit label here } else { // compensate for the whenNull that we are not visiting. _counter += 1; } return node.Update(receiver, node.HasValueMethodOpt, whenNotNull, whenNull, node.Id, node.ForceCopyOfNullableValueType, node.Type); } public override BoundNode VisitComplexConditionalReceiver(BoundComplexConditionalReceiver node) { EnsureOnlyEvalStack(); var origStack = StackDepth(); PushEvalStack(null, ExprContext.None); var cookie = GetStackStateCookie(); // implicit goto here SetStackDepth(origStack); // consequence is evaluated with original stack var valueTypeReceiver = (BoundExpression)this.Visit(node.ValueTypeReceiver); EnsureStackState(cookie); // implicit label here SetStackDepth(origStack); // alternative is evaluated with original stack var unwrappedSequence = node.ReferenceTypeReceiver; while (unwrappedSequence is BoundSequence sequence) { unwrappedSequence = sequence.Value; } if (unwrappedSequence is BoundLocal { LocalSymbol: { } localSymbol }) { ShouldNotSchedule(localSymbol); } var referenceTypeReceiver = (BoundExpression)this.Visit(node.ReferenceTypeReceiver); EnsureStackState(cookie); // implicit label here return node.Update(valueTypeReceiver, referenceTypeReceiver, node.Type); } public override BoundNode VisitUnaryOperator(BoundUnaryOperator node) { // checked(-x) is emitted as "0 - x" if (node.OperatorKind.IsChecked() && node.OperatorKind.Operator() == UnaryOperatorKind.UnaryMinus) { var origStack = StackDepth(); PushEvalStack(new BoundDefaultExpression(node.Syntax, node.Operand.Type), ExprContext.Value); BoundExpression operand = (BoundExpression)this.Visit(node.Operand); return node.Update(node.OperatorKind, operand, node.ConstantValueOpt, node.MethodOpt, node.ConstrainedToTypeOpt, node.ResultKind, node.Type); } else { return base.VisitUnaryOperator(node); } } public override BoundNode VisitTryStatement(BoundTryStatement node) { EnsureOnlyEvalStack(); var tryBlock = (BoundBlock)this.Visit(node.TryBlock); var catchBlocks = this.VisitList(node.CatchBlocks); EnsureOnlyEvalStack(); var finallyBlock = (BoundBlock)this.Visit(node.FinallyBlockOpt); EnsureOnlyEvalStack(); return node.Update(tryBlock, catchBlocks, finallyBlock, finallyLabelOpt: node.FinallyLabelOpt, node.PreferFaultHandler); } public override BoundNode VisitCatchBlock(BoundCatchBlock node) { EnsureOnlyEvalStack(); var exceptionSourceOpt = node.ExceptionSourceOpt; DeclareLocals(node.Locals, stack: 0); if (exceptionSourceOpt != null) { // runtime pushes the exception object PushEvalStack(null, ExprContext.None); _counter++; // We consume it by writing into the exception source. if (exceptionSourceOpt.Kind == BoundKind.Local) { RecordVarWrite(((BoundLocal)exceptionSourceOpt).LocalSymbol); } else { int prevStack = StackDepth(); exceptionSourceOpt = VisitExpression(exceptionSourceOpt, ExprContext.AssignmentTarget); _assignmentLocal = null; // not using this for exceptionSource SetStackDepth(prevStack); } PopEvalStack(); _counter++; } BoundStatementList filterPrologue; if (node.ExceptionFilterPrologueOpt != null) { EnsureOnlyEvalStack(); filterPrologue = (BoundStatementList)this.Visit(node.ExceptionFilterPrologueOpt); } else { filterPrologue = null; } BoundExpression boundFilter; if (node.ExceptionFilterOpt != null) { boundFilter = (BoundExpression)this.Visit(node.ExceptionFilterOpt); // the value of filter expression is consumed by the VM PopEvalStack(); _counter++; // variables allocated on stack in a filter can't be used in the catch handler EnsureOnlyEvalStack(); } else { boundFilter = null; } var boundBlock = (BoundBlock)this.Visit(node.Body); var exceptionTypeOpt = this.VisitType(node.ExceptionTypeOpt); return node.Update(node.Locals, exceptionSourceOpt, exceptionTypeOpt, filterPrologue, boundFilter, boundBlock, node.IsSynthesizedAsyncCatchAll); } public override BoundNode VisitConvertedStackAllocExpression(BoundConvertedStackAllocExpression node) { // CLI spec section 3.47 requires that the stack be empty when localloc occurs. EnsureOnlyEvalStack(); return base.VisitConvertedStackAllocExpression(node); } public override BoundNode VisitArrayInitialization(BoundArrayInitialization node) { // nontrivial construct - may use dups, metadata blob helpers etc.. EnsureOnlyEvalStack(); var initializers = node.Initializers; ArrayBuilder<BoundExpression> rewrittenInitializers = null; if (!initializers.IsDefault) { for (int i = 0; i < initializers.Length; i++) { // array itself will be pushed on the stack here. EnsureOnlyEvalStack(); var initializer = initializers[i]; var rewrittenInitializer = this.VisitExpression(initializer, ExprContext.Value); if (rewrittenInitializers == null && rewrittenInitializer != initializer) { rewrittenInitializers = ArrayBuilder<BoundExpression>.GetInstance(); rewrittenInitializers.AddRange(initializers, i); } if (rewrittenInitializers != null) { rewrittenInitializers.Add(rewrittenInitializer); } } } return node.Update(rewrittenInitializers != null ? rewrittenInitializers.ToImmutableAndFree() : initializers); } public override BoundNode VisitAddressOfOperator(BoundAddressOfOperator node) { BoundExpression visitedOperand = this.VisitExpression(node.Operand, ExprContext.Address); return node.Update(visitedOperand, node.IsManaged, node.Type); } public override BoundNode VisitReturnStatement(BoundReturnStatement node) { BoundExpression expressionOpt = (BoundExpression)this.Visit(node.ExpressionOpt); // must not have locals on stack when returning EnsureOnlyEvalStack(); return node.Update(node.RefKind, expressionOpt, @checked: node.Checked); } // Ensures that there are no stack locals. // It is done by accessing virtual "empty" local that is at the bottom of all stack locals. private void EnsureOnlyEvalStack() { RecordVarRead(empty); } private object GetStackStateCookie() { // create a dummy and start tracing it var dummy = new DummyLocal(); _dummyVariables.Add(dummy, dummy); _locals.Add(dummy, LocalDefUseInfo.GetInstance(StackDepth())); RecordDummyWrite(dummy); return dummy; } private void EnsureStackState(object cookie) { RecordVarRead(_dummyVariables[cookie]); } // called on branches and labels private void RecordBranch(LabelSymbol label) { DummyLocal dummy; if (_dummyVariables.TryGetValue(label, out dummy)) { RecordVarRead(dummy); } else { // create a dummy and start tracing it dummy = new DummyLocal(); _dummyVariables.Add(label, dummy); _locals.Add(dummy, LocalDefUseInfo.GetInstance(StackDepth())); RecordDummyWrite(dummy); } } private void RecordLabel(LabelSymbol label) { DummyLocal dummy; if (_dummyVariables.TryGetValue(label, out dummy)) { RecordVarRead(dummy); } else { // this is a backwards jump with nontrivial stack requirements. // just use empty. dummy = empty; _dummyVariables.Add(label, dummy); RecordVarRead(dummy); } } private void ShouldNotSchedule(LocalSymbol localSymbol) { LocalDefUseInfo localDefInfo; if (_locals.TryGetValue(localSymbol, out localDefInfo)) { localDefInfo.ShouldNotSchedule(); } } private void RecordVarRef(LocalSymbol local) { Debug.Assert(local.RefKind == RefKind.None, "cannot take a ref of a ref"); if (!CanScheduleToStack(local)) { return; } // if we ever take a reference of a local, it must be a real local. ShouldNotSchedule(local); } private void RecordVarRead(LocalSymbol local) { if (!CanScheduleToStack(local)) { return; } var locInfo = _locals[local]; if (locInfo.CannotSchedule) { return; } var defs = locInfo.LocalDefs; if (defs.Count == 0) { //reading before writing. locInfo.ShouldNotSchedule(); return; } // if accessing real val, check stack if (local.SynthesizedKind != SynthesizedLocalKind.OptimizerTemp) { if (locInfo.StackAtDeclaration != StackDepth() && !EvalStackHasLocal(local)) { //reading at different eval stack. locInfo.ShouldNotSchedule(); return; } } else { // dummy must be accessed on same stack. Debug.Assert(local == empty || locInfo.StackAtDeclaration == StackDepth()); } var last = defs.Count - 1; defs[last] = defs[last].WithEnd(_counter); var nextDef = new LocalDefUseSpan(_counter); defs.Add(nextDef); } private bool EvalStackHasLocal(LocalSymbol local) { var top = _evalStack.Last(); return top.Item2 == (local.RefKind == RefKind.None ? ExprContext.Value : ExprContext.Address) && top.Item1.Kind == BoundKind.Local && ((BoundLocal)top.Item1).LocalSymbol == local; } private void RecordDummyWrite(LocalSymbol local) { Debug.Assert(local.SynthesizedKind == SynthesizedLocalKind.OptimizerTemp); var locInfo = _locals[local]; // dummy must be accessed on same stack. Debug.Assert(local == empty || locInfo.StackAtDeclaration == StackDepth()); var locDef = new LocalDefUseSpan(_counter); locInfo.LocalDefs.Add(locDef); } private void RecordVarWrite(LocalSymbol local) { Debug.Assert(local.SynthesizedKind != SynthesizedLocalKind.OptimizerTemp); if (!CanScheduleToStack(local)) { return; } var locInfo = _locals[local]; if (locInfo.CannotSchedule) { return; } // check stack // -1 because real assignment "consumes, assigns, and then pushes back" the value. var evalStack = StackDepth() - 1; if (locInfo.StackAtDeclaration != evalStack) { //writing at different eval stack. locInfo.ShouldNotSchedule(); return; } var locDef = new LocalDefUseSpan(_counter); locInfo.LocalDefs.Add(locDef); } private bool CanScheduleToStack(LocalSymbol local) { return local.CanScheduleToStack && (!_debugFriendly || !local.SynthesizedKind.IsLongLived()); } private void DeclareLocals(ImmutableArray<LocalSymbol> locals, int stack) { foreach (var local in locals) { DeclareLocal(local, stack); } } private void DeclareLocal(LocalSymbol local, int stack) { if ((object)local != null) { if (CanScheduleToStack(local)) { LocalDefUseInfo info; if (!_locals.TryGetValue(local, out info)) { _locals.Add(local, LocalDefUseInfo.GetInstance(stack)); } else { Debug.Assert(local.SynthesizedKind == SynthesizedLocalKind.LoweringTemp, "only lowering temps may be sometimes reused"); if (info.StackAtDeclaration != stack) { info.ShouldNotSchedule(); } } } } } /// <summary> /// Checks whether there is a pointer indirection that may directly contribute to the final by-ref result of the visited expression. /// For example, <c>*ptr</c> or <c>ptr->Field</c>, but not <c>Method(ref *ptr)</c>. /// If such expression is assigned to a ref local, we cannot optimize that local away, so that GC can retrack the address. /// This is a conservative check (we prefer correctness over optimization). /// </summary> private sealed class PointerIndirectionMayFlowToRefResultVisitor : BoundTreeWalkerWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator { private bool _pointerIndirectionMayFlowToRefResult; private PointerIndirectionMayFlowToRefResultVisitor(int recursionDepth) : base(recursionDepth) { } public static bool Check(BoundExpression expression, int recursionDepth) { var visitor = new PointerIndirectionMayFlowToRefResultVisitor(recursionDepth); visitor.Visit(expression); return visitor._pointerIndirectionMayFlowToRefResult; } public override BoundNode Visit(BoundNode node) { if (_pointerIndirectionMayFlowToRefResult) { // No need to continue visiting nodes if the result is `true`. return node; } return base.Visit(node); } public override BoundNode VisitPointerIndirectionOperator(BoundPointerIndirectionOperator node) { _pointerIndirectionMayFlowToRefResult = true; return node; } public override BoundNode VisitCall(BoundCall node) { return node; } public override BoundNode VisitFunctionPointerInvocation(BoundFunctionPointerInvocation node) { return node; } } } // Rewrites the tree to account for destructive nature of stack local reads. // // Typically, last read stays as-is and local is destroyed by the read. // Intermediate reads are rewritten as Dups - // // NotLastUse(X_stackLocal) ===> NotLastUse(Dup) // LastUse(X_stackLocal) ===> LastUse(X_stackLocal) // internal sealed class StackOptimizerPass2 : BoundTreeRewriterWithStackGuard { private int _nodeCounter; private readonly Dictionary<LocalSymbol, LocalDefUseInfo> _info; private StackOptimizerPass2(Dictionary<LocalSymbol, LocalDefUseInfo> info) { _info = info; } public static BoundStatement Rewrite(BoundStatement src, Dictionary<LocalSymbol, LocalDefUseInfo> info) { var scheduler = new StackOptimizerPass2(info); return (BoundStatement)scheduler.Visit(src); } public override BoundNode Visit(BoundNode node) { BoundNode result; // rewriting constants may undo constant folding and make thing worse. // so we will not go into constant nodes. // CodeGen will not do that either. var asExpression = node as BoundExpression; if (asExpression != null && asExpression.ConstantValueOpt != null) { result = node; } else { result = base.Visit(node); } _nodeCounter += 1; return result; } public override BoundNode VisitBinaryOperator(BoundBinaryOperator node) { BoundExpression child = node.Left; if (child.Kind != BoundKind.BinaryOperator || child.ConstantValueOpt != null) { return base.VisitBinaryOperator(node); } // Do not blow the stack due to a deep recursion on the left. var stack = ArrayBuilder<BoundBinaryOperator>.GetInstance(); stack.Push(node); BoundBinaryOperator binary = (BoundBinaryOperator)child; while (true) { stack.Push(binary); child = binary.Left; if (child.Kind != BoundKind.BinaryOperator || child.ConstantValueOpt != null) { break; } binary = (BoundBinaryOperator)child; } var left = (BoundExpression)this.Visit(child); while (true) { binary = stack.Pop(); var right = (BoundExpression)this.Visit(binary.Right); var type = this.VisitType(binary.Type); Debug.Assert(!binary.OperatorKind.IsDynamic()); left = binary.Update(binary.OperatorKind, binary.ConstantValueOpt, binary.BinaryOperatorMethod, binary.ConstrainedToType, binary.ResultKind, left, right, type); if (stack.Count == 0) { break; } _nodeCounter += 1; } Debug.Assert((object)binary == node); stack.Free(); return left; } private static bool IsLastAccess(LocalDefUseInfo locInfo, int counter) { return locInfo.LocalDefs.Any((d) => counter == d.Start && counter == d.End); } public override BoundNode VisitLocal(BoundLocal node) { LocalDefUseInfo locInfo; if (!_info.TryGetValue(node.LocalSymbol, out locInfo)) { return base.VisitLocal(node); } // not the last access, emit Dup. if (!IsLastAccess(locInfo, _nodeCounter)) { return new BoundDup(node.Syntax, node.LocalSymbol.RefKind, node.Type); } // last access - leave the node as is. Emit will do nothing expecting the node on the stack return base.VisitLocal(node); } public override BoundNode VisitObjectCreationExpression(BoundObjectCreationExpression node) { ImmutableArray<BoundExpression> arguments = this.VisitList(node.Arguments); Debug.Assert(node.InitializerExpressionOpt == null); TypeSymbol type = this.VisitType(node.Type); return node.Update(node.Constructor, arguments, node.ArgumentNamesOpt, node.ArgumentRefKindsOpt, node.Expanded, node.ArgsToParamsOpt, node.DefaultArguments, node.ConstantValueOpt, initializerExpressionOpt: null, type); } public override BoundNode VisitAssignmentOperator(BoundAssignmentOperator node) { LocalDefUseInfo locInfo; var left = node.Left as BoundLocal; // store to something that is not special. (operands still could be rewritten) if (left == null || !_info.TryGetValue(left.LocalSymbol, out locInfo)) { return base.VisitAssignmentOperator(node); } // indirect local store is not special. (operands still could be rewritten) // NOTE: if lhs is a stack local, it will be handled as a read and possibly duped. var isIndirectLocalStore = left.LocalSymbol.RefKind != RefKind.None && !node.IsRef; if (isIndirectLocalStore) { return base.VisitAssignmentOperator(node); } // == here we have a regular write to a stack local // // we do not need to visit lhs, because we do not read the local, // just update the counter to be in sync. // // if this is the last store, we just push the rhs // otherwise record a store. // fake visiting of left _nodeCounter += 1; // visit right var right = (BoundExpression)Visit(node.Right); // do actual assignment Debug.Assert(locInfo.LocalDefs.Any((d) => _nodeCounter == d.Start && _nodeCounter <= d.End)); var isLast = IsLastAccess(locInfo, _nodeCounter); if (isLast) { if (node.IsRef && !node.WasCompilerGenerated && left.LocalSymbol.RefKind == RefKind.Ref && right is BoundArrayAccess arrayAccess && // Value types do not need runtime element type checks. !arrayAccess.Type.IsValueType) { return new BoundRefArrayAccess(arrayAccess.Syntax, arrayAccess, right.Type); } // assigned local is not used later => just emit the Right return right; } else { // assigned local used later - keep assignment. // codegen will keep value on stack when sees assignment "stackLocal = expr" return node.Update(left, right, node.IsRef, node.Type); } } #nullable enable public override BoundNode VisitCall(BoundCall node) { if (node.ReceiverOpt is BoundCall receiver1) { var calls = ArrayBuilder<BoundCall>.GetInstance(); calls.Push(node); node = receiver1; while (node.ReceiverOpt is BoundCall receiver2) { calls.Push(node); node = receiver2; } BoundExpression? rewrittenReceiver = visitReceiver(node); while (true) { rewrittenReceiver = visitArgumentsAndUpdateCall(node, rewrittenReceiver); if (!calls.TryPop(out node!)) { break; } // Visit does this after visiting each node _nodeCounter++; } calls.Free(); return rewrittenReceiver; } else { BoundExpression? rewrittenReceiver = visitReceiver(node); return visitArgumentsAndUpdateCall(node, rewrittenReceiver); } BoundExpression? visitReceiver(BoundCall node) { BoundExpression? receiverOpt = node.ReceiverOpt; if (node.Method.RequiresInstanceReceiver) { receiverOpt = (BoundExpression?)this.Visit(receiverOpt); } else { _nodeCounter++; if (receiverOpt is BoundTypeExpression { AliasOpt: null, BoundContainingTypeOpt: null, BoundDimensionsOpt: { IsEmpty: true }, Type: TypeParameterSymbol or NamedTypeSymbol { IsUnionType: true } } typeExpression) { receiverOpt = typeExpression.Update(aliasOpt: null, boundContainingTypeOpt: null, boundDimensionsOpt: ImmutableArray<BoundExpression>.Empty, typeWithAnnotations: typeExpression.TypeWithAnnotations, type: this.VisitType(typeExpression.Type)); } else if (receiverOpt is not null) { throw ExceptionUtilities.Unreachable(); } } return receiverOpt; } BoundExpression visitArgumentsAndUpdateCall(BoundCall node, BoundExpression? receiverOpt) { ImmutableArray<BoundExpression> arguments = this.VisitList(node.Arguments); TypeSymbol? type = this.VisitType(node.Type); return node.Update(receiverOpt, node.InitialBindingReceiverIsSubjectToCloning, node.Method, arguments, node.ArgumentNamesOpt, node.ArgumentRefKindsOpt, node.IsDelegateCall, node.Expanded, node.InvokedAsExtensionMethod, node.ArgsToParamsOpt, node.DefaultArguments, node.ResultKind, node.OriginalMethodsOpt, type); } } #nullable disable public override BoundNode VisitCatchBlock(BoundCatchBlock node) { var exceptionSource = node.ExceptionSourceOpt; var type = node.ExceptionTypeOpt; var filterPrologue = node.ExceptionFilterPrologueOpt; var filter = node.ExceptionFilterOpt; var body = node.Body; if (exceptionSource != null) { // runtime pushes the exception object _nodeCounter++; if (exceptionSource.Kind == BoundKind.Local) { var sourceLocal = ((BoundLocal)exceptionSource).LocalSymbol; LocalDefUseInfo locInfo; // If catch is the last access, we do not need to store the exception object. if (_info.TryGetValue(sourceLocal, out locInfo) && IsLastAccess(locInfo, _nodeCounter)) { exceptionSource = null; } } else { exceptionSource = (BoundExpression)Visit(exceptionSource); } // we consume it by writing into the local _nodeCounter++; } filterPrologue = (filterPrologue != null) ? (BoundStatementList)this.Visit(filterPrologue) : null; if (filter != null) { filter = (BoundExpression)this.Visit(filter); // the value of filter expression is consumed by the VM _nodeCounter++; } body = (BoundBlock)this.Visit(body); type = this.VisitType(type); return node.Update(node.Locals, exceptionSource, type, filterPrologue, filter, body, node.IsSynthesizedAsyncCatchAll); } } internal sealed class DummyLocal : LocalSymbol { internal override bool IsImportedFromMetadata { get { return false; } } internal override LocalDeclarationKind DeclarationKind { get { return LocalDeclarationKind.None; } } internal override SynthesizedLocalKind SynthesizedKind { get { return SynthesizedLocalKind.OptimizerTemp; } } internal override SyntaxNode ScopeDesignatorOpt { get { return null; } } internal override LocalSymbol WithSynthesizedLocalKindAndSyntax( SynthesizedLocalKind kind, SyntaxNode syntax #if DEBUG , [CallerLineNumber] int createdAtLineNumber = 0, [CallerFilePath] string createdAtFilePath = null #endif ) { throw new NotImplementedException(); } internal override SyntaxToken IdentifierToken { get { return default(SyntaxToken); } } internal override bool IsPinned { get { return false; } } internal override bool IsKnownToReferToTempIfReferenceType { get { return false; } } public override Symbol ContainingSymbol { get { throw new NotImplementedException(); } } public override TypeWithAnnotations TypeWithAnnotations { get { throw new NotImplementedException(); } } public override ImmutableArray<Location> Locations { get { throw new NotImplementedException(); } } public override ImmutableArray<SyntaxReference> DeclaringSyntaxReferences { get { throw new NotImplementedException(); } } internal override ConstantValue GetConstantValue(SyntaxNode node, LocalSymbol inProgress, BindingDiagnosticBag diagnostics) { throw new NotImplementedException(); } internal override bool IsCompilerGenerated { get { return true; } } internal override ReadOnlyBindingDiagnostic<AssemblySymbol> GetConstantValueDiagnostics(BoundExpression boundInitValue) { throw new NotImplementedException(); } internal override SyntaxNode GetDeclaratorSyntax() { throw new NotImplementedException(); } internal override bool HasSourceLocation => false; public override RefKind RefKind { get { return RefKind.None; } } internal override ScopedKind Scope => ScopedKind.None; } }