/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.DecisionDagRewriter.cs
1 246 строк
63 KB
AlekseyTs
Merge branch 'main' into Unions
09 дек 2025, 01:22
09 дек 2025, 01:22
fa80120
Код
Авторство
О чём код?
// 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; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using Microsoft.CodeAnalysis.CSharp.CodeGen; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; using static Microsoft.CodeAnalysis.CSharp.SyntheticBoundNodeFactory; namespace Microsoft.CodeAnalysis.CSharp { internal partial class LocalRewriter { /// <summary> /// A common base class for lowering a decision dag. /// </summary> private abstract partial class DecisionDagRewriter : PatternLocalRewriter { /// <summary> /// Get the builder for code in the given section of the switch. /// For an is-pattern expression, this is a singleton. /// </summary> protected abstract ArrayBuilder<BoundStatement> BuilderForSection(SyntaxNode section); /// <summary> /// The lowered decision dag. This includes all of the code to decide which pattern /// is matched, but not the code to assign to pattern variables and evaluate when clauses. /// </summary> private ArrayBuilder<BoundStatement> _loweredDecisionDag; /// <summary> /// The label in the code for the beginning of code for each node of the dag. /// </summary> private readonly PooledDictionary<BoundDecisionDagNode, LabelSymbol> _dagNodeLabels = PooledDictionary<BoundDecisionDagNode, LabelSymbol>.GetInstance(); #nullable enable // When different branches of the DAG share `when` expressions, the // shared expression will be lowered as a shared section and the `when` nodes that need // to will jump there. After the expression is evaluated, we need to jump to different // labels depending on the `when` node we came from. To achieve that, each `when` node // gets an identifier and sets a local before jumping into the shared `when` expression. internal LocalSymbol? _whenNodeIdentifierLocal; #nullable disable protected DecisionDagRewriter( SyntaxNode node, LocalRewriter localRewriter, bool generateInstrumentation) : base(node, localRewriter, generateInstrumentation) { } private void ComputeLabelSet(BoundDecisionDag decisionDag) { // Nodes with more than one predecessor are assigned a label var hasPredecessor = PooledHashSet<BoundDecisionDagNode>.GetInstance(); foreach (BoundDecisionDagNode node in decisionDag.TopologicallySortedNodes) { switch (node) { case BoundWhenDecisionDagNode w: GetDagNodeLabel(node); if (w.WhenFalse != null) { GetDagNodeLabel(w.WhenFalse); } break; case BoundLeafDecisionDagNode d: // Leaf can branch directly to the target _dagNodeLabels[node] = d.Label; break; case BoundEvaluationDecisionDagNode e: notePredecessor(e.Next); break; case BoundTestDecisionDagNode p: notePredecessor(p.WhenTrue); notePredecessor(p.WhenFalse); break; default: throw ExceptionUtilities.UnexpectedValue(node.Kind); } } hasPredecessor.Free(); return; void notePredecessor(BoundDecisionDagNode successor) { if (successor != null && !hasPredecessor.Add(successor)) { GetDagNodeLabel(successor); } } } protected new void Free() { _dagNodeLabels.Free(); base.Free(); } protected virtual LabelSymbol GetDagNodeLabel(BoundDecisionDagNode dag) { if (!_dagNodeLabels.TryGetValue(dag, out LabelSymbol label)) { _dagNodeLabels.Add(dag, label = dag is BoundLeafDecisionDagNode d ? d.Label : _factory.GenerateLabel("dagNode")); } return label; } /// <summary> /// A utility class that is used to scan a when clause to determine if it might assign a pattern variable /// declared in that case, directly or indirectly. Used to determine if we can skip the allocation of /// pattern-matching temporary variables and use user-declared pattern variables instead, because we can /// conclude that they are not mutated by a when clause while the pattern-matching automaton is running. /// </summary> protected sealed class WhenClauseMightAssignPatternVariableWalker : BoundTreeWalkerWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator { private bool _mightAssignSomething; public bool MightAssignSomething(BoundExpression expr) { if (expr == null) return false; this._mightAssignSomething = false; this.Visit(expr); return this._mightAssignSomething; } public override BoundNode Visit(BoundNode node) { // A constant expression cannot mutate anything if (node is BoundExpression { ConstantValueOpt: { } }) return null; // Stop visiting once we determine something might get assigned return this._mightAssignSomething ? null : base.Visit(node); } protected override void VisitArguments(BoundCall node) { bool mightMutate = // might be a call to a local function that assigns something node.Method.MethodKind == MethodKind.LocalFunction || // or perhaps we are passing a variable by ref and mutating it that way, e.g. `int.Parse(..., out x)` !node.ArgumentRefKindsOpt.IsDefault || // or perhaps we are calling a mutating method of a value type MethodMayMutateReceiver(node.ReceiverOpt, node.Method); if (mightMutate) _mightAssignSomething = true; else base.VisitArguments(node); } private static bool MethodMayMutateReceiver(BoundExpression receiver, MethodSymbol method) { return method != null && !method.IsStatic && !method.IsEffectivelyReadOnly && receiver.Type?.IsReferenceType == false && // methods of primitive types do not mutate their receiver !method.ContainingType.SpecialType.IsPrimitiveRecursiveStruct(); } public override BoundNode VisitPropertyAccess(BoundPropertyAccess node) { bool mightMutate = // We only need to check the get accessor because an assignment would cause _mightAssignSomething to be set to true in the caller MethodMayMutateReceiver(node.ReceiverOpt, node.PropertySymbol.GetMethod); if (mightMutate) _mightAssignSomething = true; else base.VisitPropertyAccess(node); return null; } public override BoundNode VisitAssignmentOperator(BoundAssignmentOperator node) { _mightAssignSomething = true; return null; } public override BoundNode VisitCompoundAssignmentOperator(BoundCompoundAssignmentOperator node) { _mightAssignSomething = true; return null; } public override BoundNode VisitConversion(BoundConversion node) { visitConversion(node.Conversion); if (!_mightAssignSomething) base.VisitConversion(node); return null; void visitConversion(Conversion conversion) { switch (conversion.Kind) { case ConversionKind.MethodGroup: if (conversion.Method.MethodKind == MethodKind.LocalFunction) { _mightAssignSomething = true; } break; default: if (!conversion.UnderlyingConversions.IsDefault) { conversion.AssertUnderlyingConversionsChecked(); foreach (var underlying in conversion.UnderlyingConversions) { visitConversion(underlying); if (_mightAssignSomething) return; } } break; } } } public override BoundNode VisitDelegateCreationExpression(BoundDelegateCreationExpression node) { bool mightMutate = node.MethodOpt?.MethodKind == MethodKind.LocalFunction; if (mightMutate) _mightAssignSomething = true; else base.VisitDelegateCreationExpression(node); return null; } public override BoundNode VisitAddressOfOperator(BoundAddressOfOperator node) { _mightAssignSomething = true; return null; } public override BoundNode VisitDeconstructionAssignmentOperator(BoundDeconstructionAssignmentOperator node) { _mightAssignSomething = true; return null; } public override BoundNode VisitIncrementOperator(BoundIncrementOperator node) { _mightAssignSomething = true; return null; } public override BoundNode VisitDynamicInvocation(BoundDynamicInvocation node) { // perhaps we are passing a variable by ref and mutating it that way if (!node.ArgumentRefKindsOpt.IsDefault) _mightAssignSomething = true; else base.VisitDynamicInvocation(node); return null; } public override BoundNode VisitObjectCreationExpression(BoundObjectCreationExpression node) { // perhaps we are passing a variable by ref and mutating it that way if (!node.ArgumentRefKindsOpt.IsDefault) _mightAssignSomething = true; else base.VisitObjectCreationExpression(node); return null; } public override BoundNode VisitDynamicObjectCreationExpression(BoundDynamicObjectCreationExpression node) { if (!node.ArgumentRefKindsOpt.IsDefault) _mightAssignSomething = true; else base.VisitDynamicObjectCreationExpression(node); return null; } public override BoundNode VisitObjectInitializerMember(BoundObjectInitializerMember node) { // Although ref indexers are not declarable in C#, they may be usable if (!node.ArgumentRefKindsOpt.IsDefault) _mightAssignSomething = true; else base.VisitObjectInitializerMember(node); return null; } public override BoundNode VisitIndexerAccess(BoundIndexerAccess node) { bool mightMutate = !node.ArgumentRefKindsOpt.IsDefault || // We only need to check the get accessor because an assignment would cause _mightAssignSomething to be set to true in the caller MethodMayMutateReceiver(node.ReceiverOpt, node.Indexer.GetMethod); if (mightMutate) _mightAssignSomething = true; else base.VisitIndexerAccess(node); return null; } public override BoundNode VisitDynamicIndexerAccess(BoundDynamicIndexerAccess node) { if (!node.ArgumentRefKindsOpt.IsDefault) _mightAssignSomething = true; else base.VisitDynamicIndexerAccess(node); return null; } } protected BoundDecisionDag ShareTempsIfPossibleAndEvaluateInput( BoundDecisionDag decisionDag, BoundExpression loweredSwitchGoverningExpression, ArrayBuilder<BoundStatement> result, out BoundExpression savedInputExpression) { // Note that a when-clause can contain an assignment to a // pattern variable declared in a different when-clause (e.g. in the same section, or // in a different section via the use of a local function), so we need to analyze all // of the when clauses to see if they are all simple enough to conclude that they do // not mutate pattern variables. var mightAssignWalker = new WhenClauseMightAssignPatternVariableWalker(); bool canShareTemps = !decisionDag.TopologicallySortedNodes .Any(static (node, mightAssignWalker) => node is BoundWhenDecisionDagNode w && mightAssignWalker.MightAssignSomething(w.WhenExpression), mightAssignWalker); if (canShareTemps) { decisionDag = ShareTempsAndEvaluateInput(loweredSwitchGoverningExpression, decisionDag, expr => result.Add(_factory.ExpressionStatement(expr)), out savedInputExpression); } else { // assign the input expression to its temp. BoundExpression inputTemp = _tempAllocator.GetTemp(BoundDagTemp.ForOriginalInput(loweredSwitchGoverningExpression)); Debug.Assert(inputTemp != loweredSwitchGoverningExpression); result.Add(_factory.Assignment(inputTemp, loweredSwitchGoverningExpression)); savedInputExpression = inputTemp; } return decisionDag; } protected ImmutableArray<BoundStatement> LowerDecisionDagCore(BoundDecisionDag decisionDag) { _loweredDecisionDag = ArrayBuilder<BoundStatement>.GetInstance(); ComputeLabelSet(decisionDag); ImmutableArray<BoundDecisionDagNode> sortedNodes = decisionDag.TopologicallySortedNodes; var firstNode = sortedNodes[0]; switch (firstNode) { case BoundWhenDecisionDagNode _: case BoundLeafDecisionDagNode _: // If the first node is a leaf or when clause rather than the code for the // lowered decision dag, jump there to start. _loweredDecisionDag.Add(_factory.Goto(GetDagNodeLabel(firstNode))); break; } // Code for each when clause goes in the separate code section for its switch section. LowerWhenClauses(sortedNodes); ImmutableArray<BoundDecisionDagNode> nodesToLower = sortedNodes.WhereAsArray(n => n.Kind != BoundKind.WhenDecisionDagNode && n.Kind != BoundKind.LeafDecisionDagNode); var loweredNodes = PooledHashSet<BoundDecisionDagNode>.GetInstance(); for (int i = 0, length = nodesToLower.Length; i < length; i++) { BoundDecisionDagNode node = nodesToLower[i]; // A node may have been lowered as part of a switch dispatch, but if it had a label, we'll need to lower it individually as well bool alreadyLowered = loweredNodes.Contains(node); if (alreadyLowered && !_dagNodeLabels.TryGetValue(node, out _)) { continue; } if (_dagNodeLabels.TryGetValue(node, out LabelSymbol label)) { _loweredDecisionDag.Add(_factory.Label(label)); } // If we can generate an IL switch instruction, do so if (!alreadyLowered && GenerateSwitchDispatch(node, loweredNodes)) { continue; } // If we can generate a type test and cast more efficiently as an `is` followed by a null check, do so if (GenerateTypeTestAndCast(node, loweredNodes, nodesToLower, i)) { continue; } // We pass the node that will follow so we can permit a test to fall through if appropriate BoundDecisionDagNode nextNode = ((i + 1) < length) ? nodesToLower[i + 1] : null; if (nextNode != null && loweredNodes.Contains(nextNode)) { nextNode = null; } LowerDecisionDagNode(node, nextNode); } loweredNodes.Free(); var result = _loweredDecisionDag.ToImmutableAndFree(); _loweredDecisionDag = null; return result; } /// <summary> /// If we have a type test followed by a cast to that type, and the types are reference types, /// then we can replace the pair of them by a conversion using `as` and a null check. /// </summary> /// <returns>true if we generated code for the test</returns> private bool GenerateTypeTestAndCast( BoundDecisionDagNode node, HashSet<BoundDecisionDagNode> loweredNodes, ImmutableArray<BoundDecisionDagNode> nodesToLower, int indexOfNode) { Debug.Assert(node == nodesToLower[indexOfNode]); if (node is BoundTestDecisionDagNode testNode && testNode.WhenTrue is BoundEvaluationDecisionDagNode evaluationNode && TryLowerTypeTestAndCast(testNode.Test, evaluationNode.Evaluation, out BoundExpression sideEffect, out BoundExpression test) ) { var whenTrue = evaluationNode.Next; var whenFalse = testNode.WhenFalse; bool canEliminateEvaluationNode = !this._dagNodeLabels.ContainsKey(evaluationNode); if (canEliminateEvaluationNode) loweredNodes.Add(evaluationNode); var nextNode = (indexOfNode + 2 < nodesToLower.Length) && canEliminateEvaluationNode && nodesToLower[indexOfNode + 1] == evaluationNode && !loweredNodes.Contains(nodesToLower[indexOfNode + 2]) ? nodesToLower[indexOfNode + 2] : null; _loweredDecisionDag.Add(_factory.ExpressionStatement(sideEffect)); GenerateTest(test, whenTrue, whenFalse, nextNode); return true; } return false; } private void GenerateTest(BoundExpression test, BoundDecisionDagNode whenTrue, BoundDecisionDagNode whenFalse, BoundDecisionDagNode nextNode) { // Because we have already "optimized" away tests for a constant switch expression, the test should be nontrivial. _factory.Syntax = test.Syntax; Debug.Assert(test != null); if (nextNode == whenFalse) { _loweredDecisionDag.Add(_factory.ConditionalGoto(test, GetDagNodeLabel(whenTrue), jumpIfTrue: true)); // fall through to false path } else if (nextNode == whenTrue) { _loweredDecisionDag.Add(_factory.ConditionalGoto(test, GetDagNodeLabel(whenFalse), jumpIfTrue: false)); // fall through to true path } else { _loweredDecisionDag.Add(_factory.ConditionalGoto(test, GetDagNodeLabel(whenTrue), jumpIfTrue: true)); _loweredDecisionDag.Add(_factory.Goto(GetDagNodeLabel(whenFalse))); } } /// <summary> /// Generate a switch dispatch for a contiguous sequence of dag nodes if applicable. /// Returns true if it was applicable. /// </summary> private bool GenerateSwitchDispatch(BoundDecisionDagNode node, HashSet<BoundDecisionDagNode> loweredNodes) { Debug.Assert(!loweredNodes.Contains(node)); if (!canGenerateSwitchDispatch(node)) return false; var input = ((BoundTestDecisionDagNode)node).Test.Input; ValueDispatchNode n = GatherValueDispatchNodes(node, loweredNodes, input); LowerValueDispatchNode(n, _tempAllocator.GetTemp(input)); return true; bool canGenerateSwitchDispatch(BoundDecisionDagNode node) { switch (node) { // These are the forms worth optimizing. case BoundTestDecisionDagNode { WhenFalse: BoundTestDecisionDagNode test2 } test1: return canDispatch(test1, test2); case BoundTestDecisionDagNode { WhenTrue: BoundTestDecisionDagNode test2 } test1: return canDispatch(test1, test2); default: // Other cases are just as well done with a single test. return false; } bool canDispatch(BoundTestDecisionDagNode test1, BoundTestDecisionDagNode test2) { if (this._dagNodeLabels.ContainsKey(test2)) return false; Debug.Assert(!loweredNodes.Contains(test2)); var t1 = test1.Test; var t2 = test2.Test; if (!(t1 is BoundDagValueTest || t1 is BoundDagRelationalTest)) return false; if (!(t2 is BoundDagValueTest || t2 is BoundDagRelationalTest)) return false; if (!t1.Input.Equals(t2.Input)) return false; if (t1.Input.Type.SpecialType is SpecialType.System_Double or SpecialType.System_Single) { // The optimization (using balanced switch dispatch) breaks the semantics of NaN return false; } return true; } } } private ValueDispatchNode GatherValueDispatchNodes( BoundDecisionDagNode node, HashSet<BoundDecisionDagNode> loweredNodes, BoundDagTemp input) { IConstantValueSetFactory fac = ValueSetFactory.ForInput(input); return GatherValueDispatchNodes(node, loweredNodes, input, fac); } private ValueDispatchNode GatherValueDispatchNodes( BoundDecisionDagNode node, HashSet<BoundDecisionDagNode> loweredNodes, BoundDagTemp input, IConstantValueSetFactory fac) { if (loweredNodes.Contains(node)) { bool foundLabel = this._dagNodeLabels.TryGetValue(node, out LabelSymbol label); Debug.Assert(foundLabel); return new ValueDispatchNode.LeafDispatchNode(node.Syntax, label); } if (!(node is BoundTestDecisionDagNode testNode && testNode.Test.Input.Equals(input))) { var label = GetDagNodeLabel(node); return new ValueDispatchNode.LeafDispatchNode(node.Syntax, label); } switch (testNode.Test) { case BoundDagRelationalTest relational: { loweredNodes.Add(testNode); var whenTrue = GatherValueDispatchNodes(testNode.WhenTrue, loweredNodes, input, fac); var whenFalse = GatherValueDispatchNodes(testNode.WhenFalse, loweredNodes, input, fac); return ValueDispatchNode.RelationalDispatch.CreateBalanced(testNode.Syntax, relational.Value, relational.OperatorKind, whenTrue: whenTrue, whenFalse: whenFalse); } case BoundDagValueTest value: { // Gather up the (value, label) pairs, starting with the first one loweredNodes.Add(testNode); var cases = ArrayBuilder<(ConstantValue value, LabelSymbol label)>.GetInstance(); cases.Add((value: value.Value, label: GetDagNodeLabel(testNode.WhenTrue))); BoundTestDecisionDagNode previous = testNode; while (previous.WhenFalse is BoundTestDecisionDagNode p && p.Test is BoundDagValueTest vd && vd.Input.Equals(input) && !this._dagNodeLabels.ContainsKey(p) && !loweredNodes.Contains(p)) { cases.Add((value: vd.Value, label: GetDagNodeLabel(p.WhenTrue))); loweredNodes.Add(p); previous = p; } var otherwise = GatherValueDispatchNodes(previous.WhenFalse, loweredNodes, input, fac); return PushEqualityTestsIntoTree(value.Syntax, otherwise, cases.ToImmutableAndFree(), fac); } default: { var label = GetDagNodeLabel(node); return new ValueDispatchNode.LeafDispatchNode(node.Syntax, label); } } } /// <summary> /// Push the set of equality tests down to the level of the leaves in the value dispatch tree. /// </summary> private ValueDispatchNode PushEqualityTestsIntoTree( SyntaxNode syntax, ValueDispatchNode otherwise, ImmutableArray<(ConstantValue value, LabelSymbol label)> cases, IConstantValueSetFactory fac) { if (cases.IsEmpty) return otherwise; switch (otherwise) { case ValueDispatchNode.LeafDispatchNode leaf: return new ValueDispatchNode.SwitchDispatch(syntax, cases, leaf.Label); case ValueDispatchNode.SwitchDispatch sd: return new ValueDispatchNode.SwitchDispatch(sd.Syntax, sd.Cases.Concat(cases), sd.Otherwise); case ValueDispatchNode.RelationalDispatch { Operator: var op, Value: var value, WhenTrue: var whenTrue, WhenFalse: var whenFalse } rel: var (whenTrueCases, whenFalseCases) = splitCases(cases, op, value); Debug.Assert(cases.Length == whenTrueCases.Length + whenFalseCases.Length); whenTrue = PushEqualityTestsIntoTree(syntax, whenTrue, whenTrueCases, fac); whenFalse = PushEqualityTestsIntoTree(syntax, whenFalse, whenFalseCases, fac); var result = rel.WithTrueAndFalseChildren(whenTrue: whenTrue, whenFalse: whenFalse); return result; default: throw ExceptionUtilities.UnexpectedValue(otherwise); } (ImmutableArray<(ConstantValue value, LabelSymbol label)> whenTrueCases, ImmutableArray<(ConstantValue value, LabelSymbol label)> whenFalseCases) splitCases(ImmutableArray<(ConstantValue value, LabelSymbol label)> cases, BinaryOperatorKind op, ConstantValue value) { var whenTrueBuilder = ArrayBuilder<(ConstantValue value, LabelSymbol label)>.GetInstance(); var whenFalseBuilder = ArrayBuilder<(ConstantValue value, LabelSymbol label)>.GetInstance(); op = op.Operator(); foreach (var pair in cases) { (fac.Related(op, pair.value, value) ? whenTrueBuilder : whenFalseBuilder).Add(pair); } return (whenTrueBuilder.ToImmutableAndFree(), whenFalseBuilder.ToImmutableAndFree()); } } private void LowerValueDispatchNode(ValueDispatchNode n, BoundExpression input) { switch (n) { case ValueDispatchNode.LeafDispatchNode leaf: _loweredDecisionDag.Add(_factory.Goto(leaf.Label)); return; case ValueDispatchNode.SwitchDispatch eq: LowerSwitchDispatchNode(eq, input); return; case ValueDispatchNode.RelationalDispatch rel: LowerRelationalDispatchNode(rel, input); return; default: throw ExceptionUtilities.UnexpectedValue(n); } } private void LowerRelationalDispatchNode(ValueDispatchNode.RelationalDispatch rel, BoundExpression input) { var test = MakeRelationalTest(rel.Syntax, input, rel.Operator, rel.Value); if (rel.WhenTrue is ValueDispatchNode.LeafDispatchNode whenTrue) { LabelSymbol trueLabel = whenTrue.Label; _loweredDecisionDag.Add(_factory.ConditionalGoto(test, trueLabel, jumpIfTrue: true)); LowerValueDispatchNode(rel.WhenFalse, input); } else if (rel.WhenFalse is ValueDispatchNode.LeafDispatchNode whenFalse) { LabelSymbol falseLabel = whenFalse.Label; _loweredDecisionDag.Add(_factory.ConditionalGoto(test, falseLabel, jumpIfTrue: false)); LowerValueDispatchNode(rel.WhenTrue, input); } else { LabelSymbol falseLabel = _factory.GenerateLabel("relationalDispatch"); _loweredDecisionDag.Add(_factory.ConditionalGoto(test, falseLabel, jumpIfTrue: false)); LowerValueDispatchNode(rel.WhenTrue, input); _loweredDecisionDag.Add(_factory.Label(falseLabel)); LowerValueDispatchNode(rel.WhenFalse, input); } } /// <summary> /// A comparer for sorting cases containing values of type float, double, or decimal. /// </summary> private sealed class CasesComparer : IComparer<(ConstantValue value, LabelSymbol label)> { private readonly IConstantValueSetFactory _fac; public CasesComparer(TypeSymbol type) { _fac = ValueSetFactory.ForType(type); Debug.Assert(_fac is { }); } int IComparer<(ConstantValue value, LabelSymbol label)>.Compare((ConstantValue value, LabelSymbol label) left, (ConstantValue value, LabelSymbol label) right) { var x = left.value; var y = right.value; Debug.Assert(x.Discriminator switch { ConstantValueTypeDiscriminator.Decimal => true, ConstantValueTypeDiscriminator.Single => true, ConstantValueTypeDiscriminator.Double => true, ConstantValueTypeDiscriminator.NInt => true, ConstantValueTypeDiscriminator.NUInt => true, _ => false }); Debug.Assert(y.Discriminator == x.Discriminator); // Sort NaN values into the "highest" position so they fall naturally into the last bucket // when partitioned using less-than. return isNaN(x) ? 1 : isNaN(y) ? -1 : _fac.Related(BinaryOperatorKind.LessThanOrEqual, x, y) ? (_fac.Related(BinaryOperatorKind.LessThanOrEqual, y, x) ? 0 : -1) : 1; static bool isNaN(ConstantValue value) => (value.Discriminator == ConstantValueTypeDiscriminator.Single || value.Discriminator == ConstantValueTypeDiscriminator.Double) && double.IsNaN(value.DoubleValue); } } private void LowerSwitchDispatchNode(ValueDispatchNode.SwitchDispatch node, BoundExpression input) { LabelSymbol defaultLabel = node.Otherwise; if (input.Type.IsValidV6SwitchGoverningType() || input.Type.IsSpanOrReadOnlySpanChar()) { // If we are emitting a hash table based string switch, // we need to generate a helper method for computing // string hash value in <PrivateImplementationDetails> class. bool isStringInput = input.Type.SpecialType == SpecialType.System_String; bool isSpanInput = input.Type.IsSpanChar(); bool isReadOnlySpanInput = input.Type.IsReadOnlySpanChar(); LengthBasedStringSwitchData lengthBasedDispatchOpt = null; if (isStringInput || isSpanInput || isReadOnlySpanInput) { var stringPatternInput = isStringInput ? StringPatternInput.String : (isSpanInput ? StringPatternInput.SpanChar : StringPatternInput.ReadOnlySpanChar); if (!this._localRewriter._compilation.FeatureDisableLengthBasedSwitch && this._factory.Compilation.Options.OptimizationLevel == OptimizationLevel.Release && LengthBasedStringSwitchData.Create(node.Cases) is var lengthBasedDispatch && lengthBasedDispatch.ShouldGenerateLengthBasedSwitch(node.Cases.Length) && hasLengthBasedDispatchRequiredMembers(stringPatternInput)) { lengthBasedDispatchOpt = lengthBasedDispatch; } else { EnsureStringHashFunction(node.Cases.Length, node.Syntax, stringPatternInput); } if (isStringInput) { // Report required missing member diagnostic _localRewriter.TryGetSpecialTypeMethod(node.Syntax, SpecialMember.System_String__op_Equality, out _); } } var dispatch = new BoundSwitchDispatch(node.Syntax, input, node.Cases, defaultLabel, lengthBasedDispatchOpt); _loweredDecisionDag.Add(dispatch); } else if (input.Type.IsNativeIntegerType) { // Native types need to be dispatched using a larger underlying type so that any // possible high bits are not truncated. ImmutableArray<(ConstantValue value, LabelSymbol label)> cases; switch (input.Type.SpecialType) { case SpecialType.System_IntPtr: { NamedTypeSymbol int64Type = _factory.SpecialType(SpecialType.System_Int64); Conversion c = _factory.ClassifyEmitConversion(input, int64Type); Debug.Assert(c.IsImplicit); Debug.Assert(c.IsNumeric); input = _factory.Convert(int64Type, input, c); cases = node.Cases.SelectAsArray(p => (ConstantValue.Create((long)p.value.Int32Value), p.label)); break; } case SpecialType.System_UIntPtr: { NamedTypeSymbol uint64Type = _factory.SpecialType(SpecialType.System_UInt64); Conversion c = _factory.ClassifyEmitConversion(input, uint64Type); Debug.Assert(c.IsImplicit); Debug.Assert(c.IsNumeric); input = _factory.Convert(uint64Type, input, c); cases = node.Cases.SelectAsArray(p => (ConstantValue.Create((ulong)p.value.UInt32Value), p.label)); break; } default: throw ExceptionUtilities.UnexpectedValue(input.Type); } var dispatch = new BoundSwitchDispatch(node.Syntax, input, cases, defaultLabel, lengthBasedStringSwitchDataOpt: null); _loweredDecisionDag.Add(dispatch); } else { // The emitter does not know how to produce a "switch" instruction for float, double, or decimal // (in part because there is no such instruction) so we fake it here by generating a decision tree. var lessThanOrEqualOperator = input.Type.SpecialType switch { SpecialType.System_Single => BinaryOperatorKind.FloatLessThanOrEqual, SpecialType.System_Double => BinaryOperatorKind.DoubleLessThanOrEqual, SpecialType.System_Decimal => BinaryOperatorKind.DecimalLessThanOrEqual, _ => throw ExceptionUtilities.UnexpectedValue(input.Type.SpecialType) }; var cases = node.Cases.Sort(new CasesComparer(input.Type)); lowerFloatDispatch(0, cases.Length); void lowerFloatDispatch(int firstIndex, int count) { if (count <= 3) { for (int i = firstIndex, limit = firstIndex + count; i < limit; i++) { _loweredDecisionDag.Add(_factory.ConditionalGoto(MakeValueTest(node.Syntax, input, cases[i].value), cases[i].label, jumpIfTrue: true)); } _loweredDecisionDag.Add(_factory.Goto(defaultLabel)); } else { int half = count / 2; var gt = _factory.GenerateLabel("greaterThanMidpoint"); _loweredDecisionDag.Add(_factory.ConditionalGoto(MakeRelationalTest(node.Syntax, input, lessThanOrEqualOperator, cases[firstIndex + half - 1].value), gt, jumpIfTrue: false)); lowerFloatDispatch(firstIndex, half); _loweredDecisionDag.Add(_factory.Label(gt)); lowerFloatDispatch(firstIndex + half, count - half); } } } return; bool hasLengthBasedDispatchRequiredMembers(StringPatternInput stringPatternInput) { var compilation = _localRewriter._compilation; var lengthMember = stringPatternInput switch { StringPatternInput.String => compilation.GetSpecialTypeMember(SpecialMember.System_String__Length), StringPatternInput.SpanChar => compilation.GetWellKnownTypeMember(WellKnownMember.System_Span_T__get_Length), StringPatternInput.ReadOnlySpanChar => compilation.GetWellKnownTypeMember(WellKnownMember.System_ReadOnlySpan_T__get_Length), _ => throw ExceptionUtilities.UnexpectedValue(stringPatternInput), }; if ((object)lengthMember == null || lengthMember.HasUseSiteError) { return false; } var charsMember = stringPatternInput switch { StringPatternInput.String => compilation.GetSpecialTypeMember(SpecialMember.System_String__Chars), StringPatternInput.SpanChar => compilation.GetWellKnownTypeMember(WellKnownMember.System_Span_T__get_Item), StringPatternInput.ReadOnlySpanChar => compilation.GetWellKnownTypeMember(WellKnownMember.System_ReadOnlySpan_T__get_Item), _ => throw ExceptionUtilities.UnexpectedValue(stringPatternInput), }; if ((object)charsMember == null || charsMember.HasUseSiteError) { return false; } return true; } } private enum StringPatternInput { String, SpanChar, ReadOnlySpanChar, } /// <summary> /// Checks whether we are generating a hash table based string switch and /// we need to generate a new helper method for computing string hash value. /// Creates the method if needed. /// </summary> private void EnsureStringHashFunction(int labelsCount, SyntaxNode syntaxNode, StringPatternInput stringPatternInput) { var module = _localRewriter.EmitModule; if (module == null) { // we're not generating code, so we don't need the hash function return; } // For string switch statements, we need to determine if we are generating a hash // table based jump table or a non hash jump table, i.e. linear string comparisons // with each case label. We use the Dev10 Heuristic to determine this // (see SwitchStringJumpTableEmitter.ShouldGenerateHashTableSwitch() for details). if (!CodeAnalysis.CodeGen.SwitchStringJumpTableEmitter.ShouldGenerateHashTableSwitch(labelsCount)) { return; } // If we are generating a hash table based jump table, we use a simple customizable // hash function to hash the string constants corresponding to the case labels. // See SwitchStringJumpTableEmitter.ComputeStringHash(). // We need to emit this function to compute the hash value into the compiler generated // <PrivateImplementationDetails> class. // If we have at least one string switch statement in a module that needs a // hash table based jump table, we generate a single public string hash synthesized method // that is shared across the module. // If we have already generated the helper, possibly for another switch // or on another thread, we don't need to regenerate it. var privateImplClass = module.GetPrivateImplClass(syntaxNode, _localRewriter._diagnostics.DiagnosticBag); if (privateImplClass.PrivateImplementationDetails.GetMethod(stringPatternInput switch { StringPatternInput.String => CodeAnalysis.CodeGen.PrivateImplementationDetails.SynthesizedStringHashFunctionName, StringPatternInput.SpanChar => CodeAnalysis.CodeGen.PrivateImplementationDetails.SynthesizedReadOnlySpanHashFunctionName, StringPatternInput.ReadOnlySpanChar => CodeAnalysis.CodeGen.PrivateImplementationDetails.SynthesizedSpanHashFunctionName, _ => throw ExceptionUtilities.UnexpectedValue(stringPatternInput), }) != null) { return; } // cannot emit hash method if have no access to Chars. var charsMember = stringPatternInput switch { StringPatternInput.String => _localRewriter._compilation.GetSpecialTypeMember(SpecialMember.System_String__Chars), StringPatternInput.SpanChar => _localRewriter._compilation.GetWellKnownTypeMember(WellKnownMember.System_Span_T__get_Item), StringPatternInput.ReadOnlySpanChar => _localRewriter._compilation.GetWellKnownTypeMember(WellKnownMember.System_ReadOnlySpan_T__get_Item), _ => throw ExceptionUtilities.UnexpectedValue(stringPatternInput), }; if ((object)charsMember == null || charsMember.HasUseSiteError) { return; } TypeSymbol returnType = _factory.SpecialType(SpecialType.System_UInt32); TypeSymbol paramType = stringPatternInput switch { StringPatternInput.String => _factory.SpecialType(SpecialType.System_String), StringPatternInput.SpanChar => _factory.WellKnownType(WellKnownType.System_Span_T) .Construct(_factory.SpecialType(SpecialType.System_Char)), StringPatternInput.ReadOnlySpanChar => _factory.WellKnownType(WellKnownType.System_ReadOnlySpan_T) .Construct(_factory.SpecialType(SpecialType.System_Char)), _ => throw ExceptionUtilities.UnexpectedValue(stringPatternInput), }; SynthesizedGlobalMethodSymbol method = stringPatternInput switch { StringPatternInput.String => new SynthesizedStringSwitchHashMethod(privateImplClass, returnType, paramType), StringPatternInput.SpanChar => new SynthesizedSpanSwitchHashMethod(privateImplClass, returnType, paramType, isReadOnlySpan: false), StringPatternInput.ReadOnlySpanChar => new SynthesizedSpanSwitchHashMethod(privateImplClass, returnType, paramType, isReadOnlySpan: true), _ => throw ExceptionUtilities.UnexpectedValue(stringPatternInput), }; privateImplClass.PrivateImplementationDetails.TryAddSynthesizedMethod(method.GetCciAdapter()); } #nullable enable private void LowerWhenClauses(ImmutableArray<BoundDecisionDagNode> sortedNodes) { if (!sortedNodes.Any(static n => n.Kind == BoundKind.WhenDecisionDagNode)) return; // The way the DAG is prepared, it is possible for different `BoundWhenDecisionDagNode` nodes to // share the same `WhenExpression` (same `BoundExpression` instance). // So we can't just lower each `BoundWhenDecisionDagNode` separately, as that would result in duplicate blocks // for the same `WhenExpression` and such expressions might contains labels which must be emitted once only. // For a simple `BoundWhenDecisionDagNode` (with a unique `WhenExpression`), we lower to something like: // labelToSectionScope; // if (... logic from WhenExpression ...) // { // jump to whenTrue label // } // jump to whenFalse label // For a complex `BoundWhenDecisionDagNode` (where the `WhenExpression` is shared), we lower to something like: // labelToSectionScope; // whenNodeIdentifierLocal = whenNodeIdentifier; // goto labelToWhenExpression; // // and we'll also create a section for the shared `WhenExpression` logic: // labelToWhenExpression; // if (... logic from WhenExpression ...) // { // jump to whenTrue label // } // switch on whenNodeIdentifierLocal with dispatches to whenFalse labels int nextWhenNodeIdentifier = 0; // Prepared maps for `when` nodes and expressions var whenExpressionMap = PooledDictionary<BoundExpression, (LabelSymbol LabelToWhenExpression, ArrayBuilder<BoundWhenDecisionDagNode> WhenNodes)>.GetInstance(); var whenNodeMap = PooledDictionary<BoundWhenDecisionDagNode, (LabelSymbol LabelToWhenExpression, int WhenNodeIdentifier)>.GetInstance(); foreach (BoundDecisionDagNode node in sortedNodes) { if (node is BoundWhenDecisionDagNode whenNode) { var whenExpression = whenNode.WhenExpression; if (whenExpression is not null && whenExpression.ConstantValueOpt != ConstantValue.True) { LabelSymbol labelToWhenExpression; if (whenExpressionMap.TryGetValue(whenExpression, out var whenExpressionInfo)) { labelToWhenExpression = whenExpressionInfo.LabelToWhenExpression; whenExpressionInfo.WhenNodes.Add(whenNode); } else { labelToWhenExpression = _factory.GenerateLabel("sharedWhenExpression"); var list = ArrayBuilder<BoundWhenDecisionDagNode>.GetInstance(); list.Add(whenNode); whenExpressionMap.Add(whenExpression, (labelToWhenExpression, list)); } whenNodeMap.Add(whenNode, (labelToWhenExpression, nextWhenNodeIdentifier++)); } } } // Lower nodes foreach (BoundDecisionDagNode node in sortedNodes) { if (node is BoundWhenDecisionDagNode whenNode) { if (!tryLowerAsJumpToSharedWhenExpression(whenNode)) { lowerWhenClause(whenNode); } } } // Lower shared `when` expressions foreach (var (whenExpression, (labelToWhenExpression, whenNodes)) in whenExpressionMap) { lowerWhenExpressionIfShared(whenExpression, labelToWhenExpression, whenNodes); whenNodes.Free(); } whenExpressionMap.Free(); whenNodeMap.Free(); return; bool tryLowerAsJumpToSharedWhenExpression(BoundWhenDecisionDagNode whenNode) { var whenExpression = whenNode.WhenExpression; if (!isSharedWhenExpression(whenExpression)) { return false; } LabelSymbol labelToSectionScope = GetDagNodeLabel(whenNode); ArrayBuilder<BoundStatement> sectionBuilder = BuilderForSection(whenNode.Syntax); sectionBuilder.Add(_factory.Label(labelToSectionScope)); _whenNodeIdentifierLocal ??= _factory.SynthesizedLocal(_factory.SpecialType(SpecialType.System_Int32)); var found = whenNodeMap.TryGetValue(whenNode, out var whenNodeInfo); Debug.Assert(found); // whenNodeIdentifierLocal = whenNodeIdentifier; sectionBuilder.Add(_factory.Assignment(_factory.Local(_whenNodeIdentifierLocal), _factory.Literal(whenNodeInfo.WhenNodeIdentifier))); // goto labelToWhenExpression; sectionBuilder.Add(_factory.Goto(whenNodeInfo.LabelToWhenExpression)); return true; } void lowerWhenExpressionIfShared(BoundExpression whenExpression, LabelSymbol labelToWhenExpression, ArrayBuilder<BoundWhenDecisionDagNode> whenNodes) { if (!isSharedWhenExpression(whenExpression)) { return; } var whenClauseSyntax = whenNodes[0].Syntax; var whenTrueLabel = GetDagNodeLabel(whenNodes[0].WhenTrue); Debug.Assert(whenNodes.Count > 1); Debug.Assert(whenNodes.All(n => n.Syntax == whenClauseSyntax)); Debug.Assert(whenNodes.All(n => n.WhenExpression == whenExpression)); Debug.Assert(whenNodes.All(n => n.Bindings == whenNodes[0].Bindings)); Debug.Assert(whenNodes.All(n => GetDagNodeLabel(n.WhenTrue) == whenTrueLabel)); ArrayBuilder<BoundStatement> sectionBuilder = BuilderForSection(whenClauseSyntax); sectionBuilder.Add(_factory.Label(labelToWhenExpression)); lowerBindings(whenNodes[0].Bindings, sectionBuilder); addConditionalGoto(whenExpression, whenClauseSyntax, whenTrueLabel, sectionBuilder); var whenFalseSwitchSections = ArrayBuilder<SyntheticSwitchSection>.GetInstance(); foreach (var whenNode in whenNodes) { var (_, whenNodeIdentifier) = whenNodeMap[whenNode]; Debug.Assert(whenNode.WhenFalse != null); whenFalseSwitchSections.Add(_factory.SwitchSection(whenNodeIdentifier, _factory.Goto(GetDagNodeLabel(whenNode.WhenFalse)))); } // switch (whenNodeIdentifierLocal) // { // case whenNodeIdentifier: goto falseLabelForWhenNode; // ... // } Debug.Assert(_whenNodeIdentifierLocal is not null); BoundStatement jumps = _factory.Switch(_factory.Local(_whenNodeIdentifierLocal), whenFalseSwitchSections.ToImmutableAndFree()); // We hide the jump back into the decision dag, as it is not logically part of the when clause sectionBuilder.Add(GenerateInstrumentation ? _factory.HiddenSequencePoint(jumps) : jumps); } // if (loweredWhenExpression) // { // jump to whenTrue label // } void addConditionalGoto(BoundExpression whenExpression, SyntaxNode whenClauseSyntax, LabelSymbol whenTrueLabel, ArrayBuilder<BoundStatement> sectionBuilder) { _factory.Syntax = whenClauseSyntax; BoundStatement conditionalGoto = _factory.ConditionalGoto(_localRewriter.VisitExpression(whenExpression), whenTrueLabel, jumpIfTrue: true); // Only add instrumentation (such as a sequence point) if the node is not compiler-generated. if (GenerateInstrumentation && !whenExpression.WasCompilerGenerated) { conditionalGoto = _localRewriter.Instrumenter.InstrumentSwitchWhenClauseConditionalGotoBody(whenExpression, conditionalGoto); } sectionBuilder.Add(conditionalGoto); } bool isSharedWhenExpression(BoundExpression? whenExpression) { return whenExpression is not null && whenExpressionMap.TryGetValue(whenExpression, out var whenExpressionInfo) && whenExpressionInfo.WhenNodes.Count > 1; } void lowerWhenClause(BoundWhenDecisionDagNode whenClause) { // This node is used even when there is no when clause, to record bindings. In the case that there // is no when clause, whenClause.WhenExpression and whenClause.WhenFalse are null, and the syntax for this // node is the case clause. // We need to assign the pattern variables in the code where they are in scope, so we produce a branch // to the section where they are in scope and evaluate the when clause there. var whenTrue = (BoundLeafDecisionDagNode)whenClause.WhenTrue; LabelSymbol labelToSectionScope = GetDagNodeLabel(whenClause); ArrayBuilder<BoundStatement> sectionBuilder = BuilderForSection(whenClause.Syntax); sectionBuilder.Add(_factory.Label(labelToSectionScope)); lowerBindings(whenClause.Bindings, sectionBuilder); var whenFalse = whenClause.WhenFalse; var trueLabel = GetDagNodeLabel(whenTrue); if (whenClause.WhenExpression != null && whenClause.WhenExpression.ConstantValueOpt != ConstantValue.True) { addConditionalGoto(whenClause.WhenExpression, whenClause.Syntax, trueLabel, sectionBuilder); // We hide the jump back into the decision dag, as it is not logically part of the when clause Debug.Assert(whenFalse != null); BoundStatement jump = _factory.Goto(GetDagNodeLabel(whenFalse)); sectionBuilder.Add(GenerateInstrumentation ? _factory.HiddenSequencePoint(jump) : jump); } else { Debug.Assert(whenFalse == null); sectionBuilder.Add(_factory.Goto(trueLabel)); } } void lowerBindings(ImmutableArray<BoundPatternBinding> bindings, ArrayBuilder<BoundStatement> sectionBuilder) { foreach (BoundPatternBinding binding in bindings) { BoundExpression left = _localRewriter.VisitExpression(binding.VariableAccess); // Since a switch does not add variables to the enclosing scope, the pattern variables // are locals even in a script and rewriting them should have no effect. Debug.Assert(left.Kind == BoundKind.Local && left == binding.VariableAccess); BoundExpression right = _tempAllocator.GetTemp(binding.TempContainingValue); if (left != right) { sectionBuilder.Add(_factory.Assignment(left, right)); } } } } #nullable disable /// <summary> /// Translate the decision dag for node, given that it will be followed by the translation for nextNode. /// </summary> private void LowerDecisionDagNode(BoundDecisionDagNode node, BoundDecisionDagNode nextNode) { _factory.Syntax = node.Syntax; switch (node) { case BoundEvaluationDecisionDagNode evaluationNode: { BoundExpression sideEffect = LowerEvaluation(evaluationNode.Evaluation); Debug.Assert(sideEffect != null); _loweredDecisionDag.Add(_factory.ExpressionStatement(sideEffect)); // We add a hidden sequence point after the evaluation's side-effect, which may be a call out // to user code such as `Deconstruct` or a property get, to permit edit-and-continue to // synchronize on changes. if (GenerateInstrumentation) _loweredDecisionDag.Add(_factory.HiddenSequencePoint()); if (nextNode != evaluationNode.Next) { // We only need a goto if we would not otherwise fall through to the desired state _loweredDecisionDag.Add(_factory.Goto(GetDagNodeLabel(evaluationNode.Next))); } } break; case BoundTestDecisionDagNode testNode: { BoundExpression test = base.LowerTest(testNode.Test); GenerateTest(test, testNode.WhenTrue, testNode.WhenFalse, nextNode); } break; default: throw ExceptionUtilities.UnexpectedValue(node.Kind); } } } } }