using System; using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Ariadne.Generator { /// /// Finds which of a passage's locals and parameters are kept in fields, and which of those are saved because /// they're still needed after one of its pauses. /// public class PassageLocals { public const string FieldSeparator = "__"; public readonly IMethodSymbol Passage; public readonly PassagePlan Plan; /// /// Holds every saved local and parameter, in the order the passage declares them. /// public readonly List Saved = new List(); /// /// Holds every local and parameter kept in a field: the saved ones, plus locals used in more than one /// braced segment, which couldn't see each other otherwise. /// public readonly List Hoisted = new List(); /// /// Holds what each pause needs saved. /// public readonly Dictionary> NeededAt = new Dictionary>(); // Each local is checked once, so a local needed at several pauses is reported only once. private readonly Dictionary movable = new Dictionary(SymbolEqualityComparer.Default); // Two hoisted locals can share a name when they sit in sibling scopes, so the later ones are numbered. private readonly Dictionary ordinals = new Dictionary(SymbolEqualityComparer.Default); // The variables that patterns and out arguments declare, which the writer can turn into hoisted locals. private readonly HashSet expressionVariables = new HashSet(SymbolEqualityComparer.Default); private readonly List topAliases = new List(); private readonly Dictionary> segmentAliases = new Dictionary>(); /// /// Holds whether the passage keeps its locals in a frame class of its own. /// public readonly bool OwnsFrame; // What every saved key of the passage starts with. An override's include its class, since a local it shares a // name with in the passage it overrides may be of another type, and would be read as the wrong one. private readonly string keyStem; private PassageLocals(IMethodSymbol passage, PassagePlan plan, bool ownsFrame, bool overridesPassage) { Passage = passage; Plan = plan; OwnsFrame = ownsFrame; keyStem = overridesPassage ? passage.ContainingType.Name + "." + passage.Name : passage.Name; } public static ITypeSymbol TypeOf(ISymbol symbol) { var local = symbol as ILocalSymbol; if (local != null) { return local.Type; } return ((IParameterSymbol)symbol).Type; } public bool IsSaved(ISymbol symbol) { return Saved.Contains(symbol, SymbolEqualityComparer.Default); } public bool IsHoisted(ISymbol symbol) { return Hoisted.Contains(symbol, SymbolEqualityComparer.Default); } /// /// Gets the hoisted symbols whose ref aliases go at the top of the passage: parameters, and locals declared /// directly in its body. /// public IReadOnlyList TopAliases { get { return topAliases; } } /// /// Returns the hoisted locals a braced segment uses, whose ref aliases go at the top of that segment. /// public IReadOnlyList AliasesIn(int segment) { List returnMe; if (segmentAliases.TryGetValue(segment, out returnMe)) { return returnMe; } return new ISymbol[0]; } private int OrdinalOf(ISymbol symbol) { int returnMe; ordinals.TryGetValue(symbol, out returnMe); return returnMe; } public string FieldName(ISymbol symbol) { string returnMe = Passage.Name + FieldSeparator + symbol.Name; int ordinal = OrdinalOf(symbol); if (ordinal > 0) { returnMe += FieldSeparator + ordinal; } return returnMe; } public string FieldName(SpillTemp temp) { return Passage.Name + FieldSeparator + temp.Name; } /// /// Returns the expression a passage copy reads and writes 's field through. /// public string Access(ISymbol symbol) { return PassageWriter.FrameName + "." + FieldName(symbol); } public string Access(SpillTemp temp) { return PassageWriter.FrameName + "." + FieldName(temp); } public string Key(SpillTemp temp) { return keyStem + "." + temp.Name; } /// /// Gets whether any value held in a spill temp has to be saved. /// public bool HasSavedTemps { get { return Plan.Temps.Any(temp => temp.Saved); } } public string Key(ISymbol symbol) { string returnMe = keyStem + "." + symbol.Name; int ordinal = OrdinalOf(symbol); if (ordinal > 0) { returnMe += "#" + ordinal; } return returnMe; } // Hoisting rewrites a declaration into an assignment, which only works for one written as a statement of its // own in a block. private static bool IsPlainDeclaration(ISymbol symbol) { var parameter = symbol as IParameterSymbol; if (parameter != null) { return parameter.RefKind != RefKind.Ref && parameter.RefKind != RefKind.RefReadOnlyParameter; } var local = (ILocalSymbol)symbol; var declarator = local.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as VariableDeclaratorSyntax; if (declarator?.Parent?.Parent is ForStatementSyntax) { return !local.IsRef && !local.IsConst; } var statement = declarator?.Parent?.Parent as LocalDeclarationStatementSyntax; SyntaxNode container = statement?.Parent; while (container is LabeledStatementSyntax) { container = container.Parent; } return statement != null && (container is BlockSyntax || container is SwitchSectionSyntax) && !local.IsRef && !local.IsConst && statement.UsingKeyword.IsKind(SyntaxKind.None); } // Returns why a field can't have this type, or null if it can. Walks the type arguments and array elements // without recursing, since the type may be arbitrarily nested. private static string WhyNotAFieldType(ITypeSymbol type) { var pending = new Stack(); pending.Push(type); while (pending.Count > 0) { ITypeSymbol next = pending.Pop(); if (next.IsAnonymousType) { return "an anonymous type has no name to declare the field with. Write a class or struct for " + "it, or use a tuple."; } if (next.IsRefLikeType) { return "a ref struct can't be the type of a field in a class."; } var array = next as IArrayTypeSymbol; if (array != null) { pending.Push(array.ElementType); } var named = next as INamedTypeSymbol; if (named != null) { foreach (ITypeSymbol argument in named.TypeArguments) { pending.Push(argument); } } } return null; } private bool CanMove(ISymbol symbol, List diagnostics) { bool returnMe; if (movable.TryGetValue(symbol, out returnMe)) { return returnMe; } returnMe = IsPlainDeclaration(symbol) || expressionVariables.Contains(symbol); if (!returnMe) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.UnsupportedLocal, symbol.Locations.FirstOrDefault(), symbol.Name)); } else { string whyNot = WhyNotAFieldType(TypeOf(symbol)); if (whyNot != null) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.UnsavableLocalType, symbol.Locations.FirstOrDefault(), symbol.Name, whyNot)); returnMe = false; } } movable.Add(symbol, returnMe); return returnMe; } // Each tick hands the script its context afresh, so a parameter carrying it is passed, never saved. A ref // or ref readonly parameter is passed afresh too, and the variable it refers to is the caller's to save. private static bool IsCandidate(ISymbol symbol, ScriptClass script) { var parameter = symbol as IParameterSymbol; if (parameter != null) { return !parameter.IsThis && parameter.RefKind != RefKind.Ref && parameter.RefKind != RefKind.RefReadOnlyParameter && !SymbolEqualityComparer.Default.Equals(parameter.Type, script.ContextType); } var local = symbol as ILocalSymbol; return local != null && !local.IsConst; } private static int DeclarationOrder(ISymbol symbol) { return symbol.Locations.First().SourceSpan.Start; } /// /// Holds one piece of code a resumed pause goes on to run: a run of statements, or an expression. /// private class Region { public readonly StatementSyntax First; public readonly StatementSyntax Last; public readonly ExpressionSyntax Expression; public Region(StatementSyntax first, StatementSyntax last) { First = first; Last = last; } public Region(ExpressionSyntax expression) { Expression = expression; } public DataFlowAnalysis Analyze(SemanticModel model) { if (Expression != null) { return model.AnalyzeDataFlow(Expression); } if (First == Last) { return model.AnalyzeDataFlow(First); } return model.AnalyzeDataFlow(First, Last); } } // The next trip around a loop runs its condition, body and step, but not its initializer, which would hide a // variable that a resume in the middle of the loop still needs. private static List NextTripOf(StatementSyntax loop) { var returnMe = new List(); var whileLoop = loop as WhileStatementSyntax; if (whileLoop != null) { returnMe.Add(new Region(whileLoop.Condition)); returnMe.Add(new Region(whileLoop.Statement, whileLoop.Statement)); return returnMe; } var doLoop = loop as DoStatementSyntax; if (doLoop != null) { returnMe.Add(new Region(doLoop.Statement, doLoop.Statement)); returnMe.Add(new Region(doLoop.Condition)); return returnMe; } var forLoop = (ForStatementSyntax)loop; if (forLoop.Condition != null) { returnMe.Add(new Region(forLoop.Condition)); } returnMe.Add(new Region(forLoop.Statement, forLoop.Statement)); foreach (ExpressionSyntax incrementor in forLoop.Incrementors) { returnMe.Add(new Region(incrementor)); } return returnMe; } private static List Run(StatementSyntax first, StatementSyntax last) { return new List { new Region(first, last) }; } // What a resumed pause goes on to run, in order: the rest of the block holding it, then the rest of each block // holding that, up to the body. An if's other branch never runs after the branch resumed in. Leaving a loop's // body goes round the loop again, which is one piece of its own. private static List> ContinuationOf(StatementSyntax pause, BlockSyntax body, bool includePause) { var returnMe = new List>(); StatementSyntax current = pause; bool includeCurrent = includePause; while (true) { SyntaxNode parent = current.Parent; var block = parent as BlockSyntax; if (block != null) { int index = block.Statements.IndexOf(current); int first = includeCurrent ? index : index + 1; if (first < block.Statements.Count) { returnMe.Add(Run(block.Statements[first], block.Statements.Last())); } if (block == body) { return returnMe; } current = block; includeCurrent = false; } else if (parent is LabeledStatementSyntax) { current = (StatementSyntax)parent; } else if (parent is SwitchSectionSyntax) { // A section runs on to the code after its switch. var section = (SwitchSectionSyntax)parent; int index = section.Statements.IndexOf(current); int first = includeCurrent ? index : index + 1; if (first < section.Statements.Count) { returnMe.Add(Run(section.Statements[first], section.Statements.Last())); } current = (StatementSyntax)section.Parent; includeCurrent = false; } else if (parent is WhileStatementSyntax || parent is DoStatementSyntax || parent is ForStatementSyntax) { // The body of a loop, which is a lone statement when it has no braces. if (includeCurrent) { returnMe.Add(Run(current, current)); } returnMe.Add(NextTripOf((StatementSyntax)parent)); current = (StatementSyntax)parent; includeCurrent = false; } else { // An embedded statement, as in "if (x) Yield();". if (includeCurrent) { returnMe.Add(Run(current, current)); } current = parent as IfStatementSyntax ?? (StatementSyntax)parent.Parent; includeCurrent = false; } } } // The variable a statement sets once its pause is over, which nothing from before the pause can be needed for. private static ISymbol AssignedByStatement(StatementSyntax statement, SemanticModel model) { var declaration = statement as LocalDeclarationStatementSyntax; if (declaration != null) { return model.GetDeclaredSymbol(declaration.Declaration.Variables[0]); } var assignment = (statement as ExpressionStatementSyntax)?.Expression as AssignmentExpressionSyntax; if (assignment != null && !assignment.IsKind(SyntaxKind.CoalesceAssignmentExpression) && ScriptClass.IsPlainTarget(assignment.Left)) { return model.GetSymbolInfo(assignment.Left).Symbol; } return null; } // A pause inside a statement resumes partway through it, so only the code still to run in the statement // counts. The locals it declares itself aren't needed from before, and operands already stored are not read // again. private static IEnumerable LocalsReadAfter(Pause pause, SemanticModel model) { foreach (SyntaxNode node in pause.Reads) { foreach (IdentifierNameSyntax name in node.DescendantNodesAndSelf().OfType()) { ISymbol symbol = model.GetSymbolInfo(name).Symbol; if ((symbol is ILocalSymbol || symbol is IParameterSymbol) && !IsReplaced(name, node, pause.Replacements) && !symbol.Locations.Any(location => pause.Outer.Span.Contains(location.SourceSpan))) { yield return symbol; } } } } // What an if, loop or switch runs after its header: its branches, or its body and the next trip around. private static List OwnerRegions(StatementSyntax owner) { var ifStatement = owner as IfStatementSyntax; if (ifStatement != null) { var branches = new List { new Region(ifStatement.Statement, ifStatement.Statement) }; if (ifStatement.Else != null) { branches.Add(new Region(ifStatement.Else.Statement, ifStatement.Else.Statement)); } return branches; } var switchStatement = owner as SwitchStatementSyntax; if (switchStatement != null) { var sections = new List(); foreach (SwitchSectionSyntax section in switchStatement.Sections) { foreach (SwitchLabelSyntax label in section.Labels) { var pattern = label as CasePatternSwitchLabelSyntax; if (pattern?.WhenClause != null) { sections.Add(new Region(pattern.WhenClause.Condition)); } } sections.Add(new Region(section.Statements[0], section.Statements.Last())); } return sections; } return NextTripOf(owner); } // Throws if the compiler can't analyze the code after the pause. No input is known to do that. Skipping the // pause would leave its locals unsaved and fail much later, so this is a tripwire. private List NeededAfter(Pause pause, BlockSyntax body, SemanticModel model, ScriptClass script, List diagnostics) { var needed = new HashSet(SymbolEqualityComparer.Default); var assigned = new HashSet(SymbolEqualityComparer.Default); var pieces = new List>(); if (pause.Owner != null) { // The header goes on to its body, which a loop then goes round again. needed.UnionWith(LocalsReadAfter(pause, model)); pieces.Add(OwnerRegions(pause.Owner)); pieces.AddRange(ContinuationOf(pause.Owner, body, includePause: false)); } else { if (pause.Call != null) { needed.UnionWith(LocalsReadAfter(pause, model)); ISymbol assignedLater = AssignedByStatement(pause.Statement, model); if (assignedLater != null) { needed.Remove(assignedLater); assigned.Add(assignedLater); } } else if (pause.Kind == PauseKind.Trigger) { // The arguments were read before the pause, and the result is set once it is over. ISymbol assignedLater = AssignedByStatement(pause.Statement, model); if (assignedLater != null) { assigned.Add(assignedLater); } } pieces.AddRange(ContinuationOf(pause.Outer, body, includePause: pause.Call == null && pause.Kind != PauseKind.Wait && pause.Kind != PauseKind.Trigger)); } foreach (List piece in pieces) { foreach (Region region in piece) { DataFlowAnalysis flow = region.Analyze(model); if (!flow.Succeeded) { throw new InvalidOperationException("Data flow analysis failed for the code after the pause '" + pause.Key + "' at " + pause.Statement.GetLocation().GetLineSpan() + "."); } // A later piece reads what an earlier one always writes, so that value is never from the save. foreach (ISymbol symbol in flow.DataFlowsIn) { if (!assigned.Contains(symbol)) { needed.Add(symbol); } } // A loop might not run, so only a lone run of statements is sure to assign. if (piece.Count == 1) { assigned.UnionWith(flow.AlwaysAssigned); } } } return needed.Where(symbol => IsCandidate(symbol, script) && CanMove(symbol, diagnostics)) .OrderBy(DeclarationOrder).ToList(); } private static IEnumerable UsesIn(PlanPart part) { if (part.Kind == PartKind.Branch) { yield return part.Condition; } else if (part.Kind == PartKind.Store || part.Kind == PartKind.Evaluate) { yield return part.Value; } else if (part.Kind == PartKind.Deadline || part.Kind == PartKind.TriggerBegin) { yield return part.Condition; } else if (part.Kind == PartKind.ArmTest) { yield return part.Condition; yield return part.Arm.Pattern; if (part.Arm.WhenClause != null) { yield return part.Arm.WhenClause.Condition; } } else if (part.Kind == PartKind.Pause && part.Pause.Call != null) { yield return part.Pause.Call; } else if (part.Kind == PartKind.Copy || part.Kind == PartKind.Pause) { yield return part.Statement; } else if (part.Kind == PartKind.ForInit) { var forLoop = (ForStatementSyntax)part.Statement; if (forLoop.Declaration != null) { yield return forLoop.Declaration; } foreach (ExpressionSyntax initializer in forLoop.Initializers) { yield return initializer; } } else if (part.Kind == PartKind.Dispatch) { var dispatching = (SwitchStatementSyntax)part.Statement; yield return dispatching.Expression; foreach (SwitchSectionSyntax section in dispatching.Sections) { foreach (SwitchLabelSyntax label in section.Labels) { yield return label; } } } else if (part.Kind == PartKind.ForStep) { foreach (ExpressionSyntax incrementor in ((ForStatementSyntax)part.Statement).Incrementors) { yield return incrementor; } } } private static VariableDeclarationSyntax DeclarationOf(PlanPart part) { if (part.Kind == PartKind.Copy || part.Kind == PartKind.Pause) { return (part.Statement as LocalDeclarationStatementSyntax)?.Declaration; } if (part.Kind == PartKind.ForInit) { return ((ForStatementSyntax)part.Statement).Declaration; } return null; } /// /// Holds where each local appears among the braced segments and the parts, for the locals declared in a segment. /// private class LocalUses { public readonly Dictionary> Segments = new Dictionary>(SymbolEqualityComparer.Default); public readonly Dictionary> Parts = new Dictionary>(SymbolEqualityComparer.Default); public readonly List DeclaredInSegments = new List(); /// /// Holds the variables declared in a condition that became a jump, which C# scopes to the statement. /// public readonly HashSet ConditionOwned = new HashSet(SymbolEqualityComparer.Default); /// /// Holds the variables a switch expression arm's test or a passage call's out argument declares. The /// statement that holds them may sit outside any segment, which couldn't see a variable declared in a /// segment of its own. /// public readonly HashSet ArmDeclared = new HashSet(SymbolEqualityComparer.Default); public void Note(ISymbol symbol, PlanPart part) { HashSet segments; if (!Segments.TryGetValue(symbol, out segments)) { segments = new HashSet(); Segments.Add(symbol, segments); } segments.Add(part.Segment); HashSet parts; if (!Parts.TryGetValue(symbol, out parts)) { parts = new HashSet(); Parts.Add(symbol, parts); } parts.Add(part); } } // A designation that the part itself scopes: C# lets one in a statement's header reach the rest of its block, // but not one inside a nested statement. private static IEnumerable DesignationsOf(PlanPart part) { foreach (SyntaxNode use in UsesIn(part)) { foreach (SingleVariableDesignationSyntax designation in use.DescendantNodesAndSelf().OfType()) { if (part.Kind != PartKind.Copy || designation.FirstAncestorOrSelf() == part.Statement) { yield return designation; } } } } // An operand that is read from a temp isn't a use of the locals inside it. private static bool IsReplaced(SyntaxNode name, SyntaxNode use, IReadOnlyDictionary replacements) { if (replacements == null) { return false; } for (SyntaxNode ancestor = name; ancestor != null && ancestor != use; ancestor = ancestor.Parent) { if (replacements.ContainsKey(ancestor) && !ExpressionSpiller.IsCompoundTarget(ancestor)) { return true; } } return false; } private LocalUses UsesOfScopedLocals(ScriptClass script) { var returnMe = new LocalUses(); foreach (Pause pause in Plan.Pauses) { InvocationExpressionSyntax call = pause.Call ?? script.AsPassageCall(pause.Statement)?.Invocation; if (call == null) { continue; } SemanticModel callModel = script.ModelFor(call); foreach (SingleVariableDesignationSyntax designation in call.ArgumentList.DescendantNodes().OfType()) { ISymbol declared = callModel.GetDeclaredSymbol(designation); if (declared != null && expressionVariables.Contains(declared)) { returnMe.ArmDeclared.Add(declared); } } } foreach (PlanPart part in Plan.Parts) { if (part.Segment < 0) { continue; } SemanticModel model = script.ModelFor(part.Anchor.SourceTree.GetRoot()); foreach (SyntaxNode use in UsesIn(part)) { // A test of an operand that was stored whole reads its temp, not the locals inside it. if (part.Kind == PartKind.Branch && part.Replacements != null && part.Replacements.ContainsKey(use)) { continue; } foreach (IdentifierNameSyntax name in use.DescendantNodesAndSelf().OfType()) { var local = model.GetSymbolInfo(name).Symbol as ILocalSymbol; if (local != null && !IsReplaced(name, use, part.Replacements)) { returnMe.Note(local, part); } } } VariableDeclarationSyntax declaration = DeclarationOf(part); if (declaration != null) { foreach (VariableDeclaratorSyntax declarator in declaration.Variables) { ISymbol declared = model.GetDeclaredSymbol(declarator); returnMe.Note(declared, part); returnMe.DeclaredInSegments.Add(declared); } } if (part.Kind != PartKind.Copy && part.Kind != PartKind.Branch && part.Kind != PartKind.Dispatch && part.Kind != PartKind.ArmTest) { continue; } foreach (SingleVariableDesignationSyntax designation in DesignationsOf(part)) { ISymbol declared = model.GetDeclaredSymbol(designation); if (declared == null || !expressionVariables.Contains(declared)) { continue; } if (part.Kind == PartKind.ArmTest) { returnMe.ArmDeclared.Add(declared); } if (returnMe.ArmDeclared.Contains(declared)) { continue; } returnMe.Note(declared, part); returnMe.DeclaredInSegments.Add(declared); if (part.Kind != PartKind.Copy) { returnMe.ConditionOwned.Add(declared); } } } return returnMe; } // A lambda, local function or query scopes its own variables, and a pause loop's condition has no // segment of its own to give an alias to. private void FindExpressionVariables(ScriptClass script, BlockSyntax body) { SemanticModel model = script.ModelFor(body); var pauseLoops = new HashSet( Plan.Pauses.Where(pause => pause.Kind == PauseKind.YieldLoop).Select(pause => pause.Statement)); foreach (SingleVariableDesignationSyntax designation in body.DescendantNodes().OfType()) { bool rewritable = !pauseLoops.Contains(designation.FirstAncestorOrSelf()); for (SyntaxNode ancestor = designation.Parent; ancestor != body && rewritable; ancestor = ancestor.Parent) { rewritable = !(ancestor is AnonymousFunctionExpressionSyntax) && !(ancestor is LocalFunctionStatementSyntax) && !(ancestor is QueryExpressionSyntax); } var local = model.GetDeclaredSymbol(designation) as ILocalSymbol; if (rewritable && local != null) { expressionVariables.Add(local); } } } private void Hoist(HashSet saved, LocalUses uses, List diagnostics) { var hoisted = new HashSet(saved, SymbolEqualityComparer.Default); foreach (ISymbol local in uses.DeclaredInSegments) { // A condition that became a jump no longer scopes its variables over the code it guards, so any use // outside the condition needs the hoisted copy, even in the same segment. bool crosses = uses.Segments[local].Count > 1 || (uses.ConditionOwned.Contains(local) && uses.Parts[local].Count > 1); if (crosses && !saved.Contains(local) && CanMove(local, diagnostics)) { hoisted.Add(local); } } foreach (ISymbol local in uses.ArmDeclared) { if (CanMove(local, diagnostics)) { hoisted.Add(local); } } // An out parameter is the passage's own variable until the passage ends, so it lives in a field. foreach (IParameterSymbol parameter in Passage.Parameters.Where(each => each.RefKind == RefKind.Out)) { hoisted.Add(parameter); } Saved.AddRange(saved.OrderBy(DeclarationOrder)); Hoisted.AddRange(hoisted.OrderBy(DeclarationOrder)); foreach (IGrouping sameName in Hoisted.GroupBy(symbol => symbol.Name)) { int ordinal = 0; foreach (ISymbol symbol in sameName) { ordinals[symbol] = ordinal++; } } var inSegments = new HashSet(uses.DeclaredInSegments, SymbolEqualityComparer.Default); foreach (ISymbol symbol in Hoisted) { if (!inSegments.Contains(symbol)) { topAliases.Add(symbol); continue; } foreach (int segment in uses.Segments[symbol].OrderBy(segment => segment)) { List aliases; if (!segmentAliases.TryGetValue(segment, out aliases)) { aliases = new List(); segmentAliases.Add(segment, aliases); } aliases.Add(symbol); } } } public static PassageLocals Find(ScriptClass script, IMethodSymbol passage, List diagnostics) { var returnMe = new PassageLocals(passage, PassagePlan.Build(script, passage, diagnostics), script.OwnsFrame(passage), overridesPassage: script.IsVirtualPassage(passage) && passage.IsOverride); BlockSyntax body = script.Methods[passage].Body; if (body == null) { return returnMe; } SemanticModel model = script.ModelFor(body); returnMe.FindExpressionVariables(script, body); var saved = new HashSet(SymbolEqualityComparer.Default); foreach (Pause pause in returnMe.Plan.Pauses) { List needed = returnMe.NeededAfter(pause, body, model, script, diagnostics); returnMe.NeededAt[pause] = needed; saved.UnionWith(needed); } returnMe.Hoist(saved, returnMe.UsesOfScopedLocals(script), diagnostics); return returnMe; } } }