using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Ariadne.Generator
{
public enum PauseKind
{
Yield,
YieldLoop,
Wait,
Trigger,
PassageCall,
}
public enum PartKind
{
Copy,
Branch,
Jump,
Label,
Pause,
ForInit,
ForStep,
Dispatch,
Store,
Deadline,
TriggerBegin,
Evaluate,
ArmTest,
}
///
/// A place a passage can pause and later resume.
///
public class Pause
{
public readonly PauseKind Kind;
///
/// Holds the pausing statement with its labels removed.
///
public readonly StatementSyntax Statement;
///
/// Holds the pausing statement with the labels in front of it.
///
public readonly StatementSyntax Outer;
public readonly string Key;
///
/// Holds the label that resuming at the key jumps to.
///
public readonly string Label;
///
/// Holds the call this pause waits in when the call is part of a bigger statement, or null.
///
public readonly InvocationExpressionSyntax Call;
///
/// Holds the temp that receives the value of .
///
public readonly SpillTemp Temp;
///
/// Holds the temps that have to be saved while this pause lasts.
///
public readonly IReadOnlyList LiveTemps;
///
/// Holds the code that still runs once a spilled pause is over: its own call, the later steps, and what is left
/// of the statement. The locals they read are what the pause has to save.
///
public readonly IReadOnlyList Reads;
///
/// Holds the call this pause waits in when it's to a passage of another arc, or null.
///
public readonly ForeignSite Foreign;
///
/// Holds the operands of that code which are read from temps, so what is inside them is not read again.
///
public readonly IReadOnlyDictionary Replacements;
///
/// Holds the if, loop or switch whose header this pause is in, or null for a pause in a plain statement.
///
public readonly StatementSyntax Owner;
public Pause(PauseKind kind, StatementSyntax statement, StatementSyntax outer, string key, string label,
InvocationExpressionSyntax call = null, SpillTemp temp = null, IReadOnlyList liveTemps = null,
IReadOnlyList reads = null, IReadOnlyDictionary replacements = null,
StatementSyntax owner = null, ForeignSite foreign = null)
{
Foreign = foreign;
Owner = owner;
Reads = reads;
Replacements = replacements;
Kind = kind;
Statement = statement;
Outer = outer;
Key = key;
Label = label;
Call = call;
Temp = temp;
LiveTemps = liveTemps ?? new SpillTemp[0];
}
}
///
/// One piece of a passage's flattened code, which the writer turns into lines.
///
public class PlanPart
{
public readonly PartKind Kind;
///
/// Holds the statement a Copy copies, the for a ForInit or ForStep writes, or null.
///
public readonly StatementSyntax Statement;
public readonly Pause Pause;
///
/// Holds the label a Label defines or a Branch or Jump goes to.
///
public readonly string Name;
///
/// Holds the condition a Branch tests.
///
public readonly ExpressionSyntax Condition;
///
/// Holds whether a Branch jumps when its test fails.
///
public readonly bool Negate;
///
/// Holds whether a Branch tests its condition for null instead of for truth.
///
public readonly NullCheck NullCheck;
///
/// Holds where a break in a Copy's statement jumps to, if that break belongs to a flattened loop.
///
public readonly string BreakLabel;
///
/// Holds where a continue in a Copy's statement jumps to, if that continue belongs to a flattened loop.
///
public readonly string ContinueLabel;
///
/// Holds, for a Copy, the label each goto case or goto default in its statement now jumps to, in source order.
/// A label is null when no section matches.
///
public readonly IReadOnlyList CaseJumps;
///
/// Holds the label of each section of the switch a Dispatch jumps into, by section.
///
public readonly IReadOnlyList SectionLabels;
///
/// Holds the operand a Store keeps, and the temp it keeps it in.
///
public readonly ExpressionSyntax Value;
public readonly SpillTemp Temp;
///
/// Holds the switch expression arm an ArmTest checks its condition against.
///
public readonly SwitchExpressionArmSyntax Arm;
///
/// Holds the operands of the part's code that are read from temps instead, or null if there are none.
///
public readonly IReadOnlyDictionary Replacements;
///
/// Holds the code the part's lines line up with.
///
public readonly Location Anchor;
///
/// Holds whether the part sits in a braced segment, rather than directly in the passage.
///
public readonly bool Scoped;
///
/// Holds which braced segment the part is in, or -1 if it isn't in one.
///
public int Segment = -1;
private PlanPart(PartKind kind, StatementSyntax statement, Pause pause, string name, Location anchor,
bool scoped, ExpressionSyntax condition = null, bool negate = false, string breakLabel = null,
string continueLabel = null, IReadOnlyList caseJumps = null,
IReadOnlyList sectionLabels = null, ExpressionSyntax value = null, SpillTemp temp = null,
IReadOnlyDictionary replacements = null, NullCheck nullCheck = NullCheck.None,
SwitchExpressionArmSyntax arm = null)
{
Arm = arm;
NullCheck = nullCheck;
CaseJumps = caseJumps;
SectionLabels = sectionLabels;
Value = value;
Temp = temp;
Replacements = replacements;
Kind = kind;
Statement = statement;
Pause = pause;
Name = name;
Anchor = anchor;
Scoped = scoped;
Condition = condition;
Negate = negate;
BreakLabel = breakLabel;
ContinueLabel = continueLabel;
}
public static PlanPart Copy(StatementSyntax statement, bool scoped, string breakLabel, string continueLabel,
IReadOnlyList caseJumps, IReadOnlyDictionary replacements = null)
{
return new PlanPart(PartKind.Copy, statement, null, null, statement.GetLocation(), scoped,
breakLabel: breakLabel, continueLabel: continueLabel, caseJumps: caseJumps,
replacements: replacements);
}
public static PlanPart Store(ExpressionSyntax value, SpillTemp temp, bool scoped,
IReadOnlyDictionary replacements)
{
return new PlanPart(PartKind.Store, null, null, null, value.GetLocation(), scoped, value: value,
temp: temp, replacements: replacements);
}
///
/// Makes a part that keeps the game time that is from now in a temp.
///
public static PlanPart Deadline(ExpressionSyntax span, SpillTemp temp, bool scoped,
IReadOnlyDictionary replacements = null)
{
return new PlanPart(PartKind.Deadline, null, null, null, span.GetLocation(), scoped, condition: span,
temp: temp, replacements: replacements);
}
///
/// Makes a part that starts a wait on the triggers lists, in .
///
public static PlanPart TriggerBegin(InvocationExpressionSyntax call, SpillTemp temp, bool scoped)
{
return new PlanPart(PartKind.TriggerBegin, null, null, null, call.GetLocation(), scoped, condition: call,
temp: temp);
}
public static PlanPart Dispatch(SwitchStatementSyntax statement, IReadOnlyList sectionLabels,
IReadOnlyDictionary replacements = null)
{
return new PlanPart(PartKind.Dispatch, statement, null, null, statement.SwitchKeyword.GetLocation(), true,
sectionLabels: sectionLabels, replacements: replacements);
}
///
/// Makes a part that goes to unless the value matches the arm's pattern and guard.
///
public static PlanPart ArmTest(ExpressionSyntax value, SwitchExpressionArmSyntax arm, string target,
IReadOnlyDictionary replacements)
{
return new PlanPart(PartKind.ArmTest, null, null, target, arm.GetLocation(), true, condition: value,
replacements: replacements, arm: arm);
}
///
/// Makes a part that evaluates an expression for what it does and drops its value.
///
public static PlanPart Evaluate(ExpressionSyntax value, Location anchor,
IReadOnlyDictionary replacements)
{
return new PlanPart(PartKind.Evaluate, null, null, null, anchor, true, value: value,
replacements: replacements);
}
public static PlanPart Branch(ExpressionSyntax condition, bool negate, string target, Location anchor,
NullCheck nullCheck = NullCheck.None, IReadOnlyDictionary replacements = null)
{
return new PlanPart(PartKind.Branch, null, null, target, anchor, true, condition, negate,
nullCheck: nullCheck, replacements: replacements);
}
public static PlanPart Jump(string target, Location anchor)
{
return new PlanPart(PartKind.Jump, null, null, target, anchor, true);
}
public static PlanPart Label(string name, Location anchor)
{
return new PlanPart(PartKind.Label, null, null, name, anchor, false);
}
public static PlanPart ForPause(Pause pause, bool scoped,
IReadOnlyDictionary replacements = null)
{
if (pause.Call != null)
{
return new PlanPart(PartKind.Pause, null, pause, null, pause.Call.GetLocation(), scoped,
replacements: replacements);
}
return new PlanPart(PartKind.Pause, pause.Statement, pause, null, pause.Statement.GetLocation(), scoped);
}
public static PlanPart ForInit(ForStatementSyntax statement,
IReadOnlyDictionary replacements = null)
{
return new PlanPart(PartKind.ForInit, statement, null, null, statement.ForKeyword.GetLocation(), true,
replacements: replacements);
}
public static PlanPart ForStep(ForStatementSyntax statement)
{
return new PlanPart(PartKind.ForStep, statement, null, null, statement.ForKeyword.GetLocation(), true);
}
}
///
/// A passage's body laid out as the code the writer emits: its pauses, and the ifs, blocks and loops that hold a
/// pause turned into jumps, since C# can't jump into a block.
///
public class PassagePlan
{
///
/// Holds the pauses in the order they appear in the source.
///
public readonly List Pauses = new List();
public readonly List Parts = new List();
///
/// Holds the values kept in fields because an expression with a pause in it needs them afterward.
///
public readonly List Temps = new List();
///
/// Holds the calls to passages of other arcs.
///
public readonly List Sites = new List();
///
/// Holds, for each labeled statement that a goto jumps to, the label the flattened passage has for it.
///
public readonly Dictionary LabelNames =
new Dictionary();
///
/// The labels that the break, continue and goto case statements of the flattened loop or switch being visited
/// jump to.
///
private class JumpTargets
{
public readonly string Break;
public readonly string Continue;
///
/// Holds the section label of each case constant of the enclosing switch, or null outside one.
///
public readonly Dictionary Cases;
public readonly string Default;
public JumpTargets(string breakLabel, string continueLabel, Dictionary cases,
string defaultLabel)
{
Break = breakLabel;
Continue = continueLabel;
Cases = cases;
Default = defaultLabel;
}
}
// Stands for a case null, which a dictionary can't hold as a key.
private static readonly object NullCase = new object();
private struct Work
{
public StatementSyntax Statement;
public PlanPart Part;
public bool Scoped;
public JumpTargets Loop;
}
private readonly ScriptClass script;
private readonly List diagnostics;
private readonly HashSet holders = new HashSet();
private readonly HashSet keys = new HashSet();
private readonly Dictionary pausesPerLabel = new Dictionary();
private readonly Stack pending = new Stack();
// The labeled statements some goto jumps to. Only those need a label in the flattened code.
private readonly HashSet jumpedTo = new HashSet();
// The labeled statements visited since the last part was added. The next part or pause name they get is
// where a goto to them lands.
private readonly List unplaced = new List();
private readonly HashSet placedNames = new HashSet();
// The labeled statements the plan flattened. Any other label in the passage is copied with its statement.
private readonly HashSet flattened = new HashSet();
// The step the statements being visited belong to: the nearest label above them.
private string stepLabel;
private int ifCount;
private int waitCount;
private int triggerWaitCount;
private int loopCount;
private int switchCount;
private int spillConditionals;
private readonly string passageName;
private PassagePlan(ScriptClass script, List diagnostics, string passageName)
{
this.script = script;
this.diagnostics = diagnostics;
this.passageName = passageName;
}
// The expressions in a statement that a pause is lifted out of, in the order they run. Returns null for a
// statement that has none, or that has a shape that can't be spilled.
private static IReadOnlyList SpillRoots(StatementSyntax statement)
{
var declaration = statement as LocalDeclarationStatementSyntax;
if (declaration != null && declaration.Declaration.Variables.Count > 1)
{
if (declaration.IsConst || !declaration.UsingKeyword.IsKind(SyntaxKind.None))
{
return null;
}
return declaration.Declaration.Variables
.Where(variable => variable.Initializer != null)
.Select(variable => variable.Initializer.Value)
.ToList();
}
ExpressionSyntax root = SpillRoot(statement);
return root == null ? null : new[] { root };
}
// The expression in a statement that a pause was lifted out of.
private static ExpressionSyntax SpillRoot(StatementSyntax statement)
{
var expression = statement as ExpressionStatementSyntax;
if (expression != null)
{
var assignment = expression.Expression as AssignmentExpressionSyntax;
if (assignment != null && assignment.IsKind(SyntaxKind.SimpleAssignmentExpression)
&& ScriptClass.IsPlainTarget(assignment.Left))
{
return assignment.Right;
}
return expression.Expression;
}
var declaration = statement as LocalDeclarationStatementSyntax;
if (declaration != null)
{
if (declaration.Declaration.Variables.Count != 1
|| declaration.IsConst
|| !declaration.UsingKeyword.IsKind(SyntaxKind.None)
|| declaration.Declaration.Type is RefTypeSyntax)
{
return null;
}
return declaration.Declaration.Variables[0].Initializer?.Value;
}
return (statement as ReturnStatementSyntax)?.Expression;
}
private bool IsSpillable(StatementSyntax statement)
{
IReadOnlyList roots = SpillRoots(statement);
return roots != null && roots.Any(root => root.DescendantNodesAndSelf().OfType()
.Any(invocation => script.AnyPassageCalled(invocation) != null));
}
private bool IsPause(StatementSyntax statement)
{
return script.IsYieldStatement(statement)
|| script.AsSpanWait(statement) != null
|| script.AsTriggerWait(statement) != null
|| script.AsYieldLoop(statement) != null
|| script.AsPassageCall(statement) != null
|| IsSpillable(statement);
}
private static bool IsFlattenable(SyntaxNode node)
{
return node is BlockSyntax
|| node is IfStatementSyntax
|| node is WhileStatementSyntax
|| node is DoStatementSyntax
|| node is ForStatementSyntax
|| node is SwitchStatementSyntax;
}
// Only blocks, ifs and loops on a path of the same from the body down to a pause are rewritten. A pause under
// anything else is reported where that construct is copied.
private void FindHolders(BlockSyntax body)
{
var chain = new List();
foreach (StatementSyntax candidate in body.DescendantNodes().OfType())
{
bool ownsCall = HeaderOf(candidate).Any(header => ContainsPassageCall(header));
if (!ownsCall && !IsPause(candidate))
{
continue;
}
chain.Clear();
if (ownsCall)
{
chain.Add(candidate);
}
bool reachesBody = true;
SyntaxNode node = candidate;
while (node.Parent != body)
{
SyntaxNode parent = node.Parent;
if (IsFlattenable(parent))
{
chain.Add((StatementSyntax)parent);
}
else if (!(parent is ElseClauseSyntax) && !(parent is LabeledStatementSyntax)
&& !(parent is SwitchSectionSyntax))
{
reachesBody = false;
break;
}
node = parent;
}
if (reachesBody)
{
holders.UnionWith(chain);
}
}
}
private void Report(DiagnosticDescriptor descriptor, Location location, params object[] arguments)
{
diagnostics.Add(Diagnostic.Create(descriptor, location, arguments));
}
// The expressions an if, loop or switch evaluates around its body.
private static IEnumerable HeaderOf(StatementSyntax statement)
{
var ifStatement = statement as IfStatementSyntax;
if (ifStatement != null)
{
yield return ifStatement.Condition;
}
var whileStatement = statement as WhileStatementSyntax;
if (whileStatement != null)
{
yield return whileStatement.Condition;
}
var doStatement = statement as DoStatementSyntax;
if (doStatement != null)
{
yield return doStatement.Condition;
}
var switchStatement = statement as SwitchStatementSyntax;
if (switchStatement != null)
{
yield return switchStatement.Expression;
}
var forStatement = statement as ForStatementSyntax;
if (forStatement != null)
{
foreach (VariableDeclaratorSyntax declarator in
forStatement.Declaration?.Variables ?? default(SeparatedSyntaxList))
{
if (declarator.Initializer != null)
{
yield return declarator.Initializer.Value;
}
}
foreach (ExpressionSyntax initializer in forStatement.Initializers)
{
yield return initializer;
}
if (forStatement.Condition != null)
{
yield return forStatement.Condition;
}
foreach (ExpressionSyntax incrementor in forStatement.Incrementors)
{
yield return incrementor;
}
}
}
// A call that is the whole expression has already run by the time its pause finishes.
private bool IsPassageCall(ExpressionSyntax expression)
{
var invocation = expression as InvocationExpressionSyntax;
return invocation != null && script.AnyPassageCalled(invocation) != null;
}
private bool ContainsPassageCall(SyntaxNode node)
{
return node.DescendantNodesAndSelf().OfType()
.Any(invocation => script.AnyPassageCalled(invocation) != null);
}
private bool ContainsPause(SyntaxNode node)
{
return node.DescendantNodesAndSelf().OfType()
.Any(invocation => script.IsYield(invocation) || script.IsWait(invocation)
|| script.AnyPassageCalled(invocation) != null);
}
private const string StatementAdvice = "A pause must be a statement of its own.";
// Names a construct a pause can't be inside, and what to write instead.
private static bool TryDescribe(SyntaxNode node, out string construct, out string advice)
{
construct = null;
advice = null;
if (node is AnonymousFunctionExpressionSyntax || node is LocalFunctionStatementSyntax)
{
construct = "a lambda or local function";
advice = "It runs as a separate method, so the script can't be suspended from inside it.";
}
else if (node is UsingStatementSyntax)
{
construct = "a using statement";
advice = "A pause would dispose the resource, and a resource can't be saved. Pause outside it.";
}
else if (node is LockStatementSyntax)
{
construct = "a lock statement";
advice = "A pause would release the lock, and a lock can't be held across ticks. Pause outside it.";
}
else if (node is TryStatementSyntax)
{
construct = "a try statement";
advice = "A pause would run its finally block, and a save can't hold a half-run try. Pause outside it.";
}
else if (node is FixedStatementSyntax)
{
construct = "a fixed statement";
advice = "The pin would end at the pause, and a pointer can't be saved. Pause outside it.";
}
else if (node is UnsafeStatementSyntax || node is CheckedStatementSyntax)
{
construct = "an unsafe, checked or unchecked block";
advice = "A pause can't keep that context yet. Pause outside the block.";
}
else if (node is CommonForEachStatementSyntax)
{
construct = "a foreach loop";
advice = "An enumerator can't be saved, and the collection may change while the script is paused. " +
"Write an index for loop instead.";
}
return construct != null;
}
// The innermost construct around the invocation, up to and including root, that can't hold a pause.
private static void DescribeContext(InvocationExpressionSyntax invocation, SyntaxNode root, string where,
out string construct, out string advice)
{
for (SyntaxNode current = invocation.Parent; current != null && invocation != root;
current = current.Parent)
{
if (TryDescribe(current, out construct, out advice))
{
return;
}
if (current == root)
{
break;
}
}
construct = where ?? "an expression";
advice = StatementAdvice;
}
// A pause the plan doesn't understand would compile but throw at runtime.
private void ReportNestedPauses(SyntaxNode node, string where = null)
{
foreach (InvocationExpressionSyntax invocation in
node.DescendantNodesAndSelf().OfType())
{
IMethodSymbol passage = script.AnyPassageCalled(invocation);
bool wait = script.IsWait(invocation);
if (!script.IsYield(invocation) && !wait && passage == null)
{
continue;
}
string construct;
string advice;
DescribeContext(invocation, node, where, out construct, out advice);
if (passage != null && !passage.ReturnsVoid && advice == StatementAdvice)
{
advice = "A pause can't be lifted out of this kind of expression yet. Assign the call's result to " +
"a local on a line of its own, then use the local.";
}
if (wait)
{
Report(ScriptDiagnostics.MisplacedWait, invocation.GetLocation(), construct, advice);
}
else if (passage == null)
{
Report(ScriptDiagnostics.MisplacedYield, invocation.GetLocation(), construct, advice);
}
else
{
Report(ScriptDiagnostics.MisplacedPassageCall, invocation.GetLocation(), passage.Name, construct,
advice);
}
}
}
// A using declaration is disposed when its block ends, which a pause after it brings forward to the pause.
private void ReportUsingDeclaration(StatementSyntax statement)
{
var declaration = statement as LocalDeclarationStatementSyntax;
var block = statement.Parent as BlockSyntax;
if (declaration == null || declaration.UsingKeyword.IsKind(SyntaxKind.None) || block == null)
{
return;
}
for (int i = block.Statements.IndexOf(statement) + 1; i < block.Statements.Count; ++i)
{
if (ContainsPause(block.Statements[i]))
{
Report(ScriptDiagnostics.UsingDeclarationAcrossPause, declaration.GetLocation());
return;
}
}
}
private bool ClaimKey(string key, Location location)
{
if (keys.Add(key))
{
return true;
}
Report(ScriptDiagnostics.DuplicateKey, location, key);
return false;
}
// A step's first pause is keyed by its label alone, so the common case's save key is exactly what the user
// wrote. A label can't contain '#', hence the second pause's key "build#1" but label "build__1".
private void NextName(out string key, out string label)
{
int count;
pausesPerLabel.TryGetValue(stepLabel, out count);
pausesPerLabel[stepLabel] = count + 1;
key = stepLabel;
label = stepLabel;
if (count > 0)
{
key = stepLabel + "#" + count;
label = stepLabel + "__" + count;
}
// A second step with the same label name in a sibling block gets a different label, so a goto to it
// has to be pointed there.
foreach (LabeledStatementSyntax labeled in unplaced)
{
if (jumpedTo.Contains(labeled))
{
LabelNames[labeled] = label;
}
}
unplaced.Clear();
}
// A labeled statement that isn't a pause has no label in the flattened code, so a goto to it gets one here.
// The name is its own, since a pause later in the same step may need the user's name.
private void PlaceLabels(Location anchor)
{
foreach (LabeledStatementSyntax labeled in unplaced)
{
if (!jumpedTo.Contains(labeled))
{
continue;
}
string baseName = labeled.Identifier.ValueText + "__target";
string name = baseName;
for (int attempt = 1; !placedNames.Add(name); ++attempt)
{
name = baseName + attempt;
}
LabelNames[labeled] = name;
Parts.Add(PlanPart.Label(name, anchor));
}
unplaced.Clear();
}
// A call pulled out of an expression is keyed by its statement's label, or by the passage's name without one.
private void NameSpilledCall(
InvocationExpressionSyntax call, string ownLabel, out string key, out string label)
{
key = script.AnyPassageCalled(call).Name;
label = key;
if (ownLabel != null)
{
NextName(out key, out label);
}
}
// A call to another arc's passage, to a passage that owns a frame, or through base has a site for its receiver
// and frames, wherever it is called. A passage of this arc that uses the arc's frame has none.
private ForeignSite SiteFor(InvocationExpressionSyntax call, string key)
{
IMethodSymbol passage = script.AnyPassageCalled(call);
ForeignSite returnMe = null;
if (script.ForeignPassageCalled(call) != null)
{
returnMe = ForeignSite.Of(script, passageName, call, key, passage);
}
else if (script.IsBaseCall(call))
{
returnMe = ForeignSite.OfBase(script, passageName, call, key, passage);
}
else if (script.OwnsFrame(passage))
{
returnMe = ForeignSite.OfOwnedFrame(script, passageName, call, key, passage);
}
if (returnMe != null)
{
Sites.Add(returnMe);
}
return returnMe;
}
private void Add(PlanPart part)
{
if (unplaced.Count > 0)
{
PlaceLabels(part.Anchor);
}
Parts.Add(part);
}
private void AddPause(Pause pause, bool scoped,
IReadOnlyDictionary replacements = null)
{
Pauses.Add(pause);
Add(PlanPart.ForPause(pause, scoped, replacements));
}
private void VisitYield(StatementSyntax outer, StatementSyntax statement, bool scoped)
{
var invocation = (InvocationExpressionSyntax)((ExpressionStatementSyntax)statement).Expression;
Location where = invocation.GetLocation();
if (stepLabel == null)
{
Report(ScriptDiagnostics.UnlabeledPause, where);
return;
}
string key;
string label;
NextName(out key, out label);
if (!ClaimKey(key, where))
{
return;
}
// The label sits before the pause, so a `goto` to the step pauses again.
var pause = new Pause(PauseKind.Yield, statement, outer, key, label + "__resume");
Add(PlanPart.Label(label, statement.GetLocation()));
AddPause(pause, scoped);
Add(PlanPart.Label(pause.Label, statement.GetLocation()));
}
// The pause is the deadline's test, so a deadline already passed never pauses. A passage call in the span
// runs first, and the deadline counts from when it finishes.
private void VisitWait(StatementSyntax outer, StatementSyntax statement, InvocationExpressionSyntax call,
string ownLabel, bool scoped)
{
Location where = call.GetLocation();
if (stepLabel == null)
{
Report(ScriptDiagnostics.UnlabeledPause, where);
return;
}
ExpressionSyntax span = call.ArgumentList.Arguments[0].Expression;
bool nestedYield = span.DescendantNodesAndSelf().OfType()
.Any(invocation => script.IsYield(invocation) || script.IsWait(invocation));
SpillSequence spill = null;
if (!nestedYield && ContainsPassageCall(span))
{
spill = ExpressionSpiller.Spill(script, script.ModelFor(statement), statement, span, Temps.Count + 1,
spillConditionals + 1);
}
if (nestedYield || (spill == null && ContainsPause(span)))
{
ReportNestedPauses(span, "a Wait's argument");
return;
}
if (spill != null)
{
AddSpillSteps(spill, outer, statement, ownLabel, scoped, statement);
}
string key;
string label;
NextName(out key, out label);
if (!ClaimKey(key, where))
{
return;
}
ITypeSymbol duration = script.ModelFor(statement).Compilation.GetTypeByMetadataName("Amphora.Duration");
++waitCount;
var deadline = new SpillTemp("__wait" + waitCount, duration, where) { Saved = true };
Temps.Add(deadline);
// The label before the deadline is where a goto to the step lands, and the one after it is where resuming
// lands, so a resume tests the deadline kept rather than making a new one.
var pause = new Pause(PauseKind.Wait, statement, outer, key, label + "__resume",
liveTemps: new[] { deadline });
Add(PlanPart.Label(label, statement.GetLocation()));
Add(PlanPart.Deadline(span, deadline, scoped, spill?.Replacements));
Add(PlanPart.Label(pause.Label, statement.GetLocation()));
AddPause(pause, scoped);
}
// What a wait's TriggerWait can hold. The generator can't reference the runtime type that sets the limit.
private const int MaximumWaitedTriggers = 32;
// The frame builds a TriggerWait for each wait, sized for the triggers the call lists, so they can't come as an
// array whose length is known only when the script runs.
private string TriggerListProblem(InvocationExpressionSyntax call, List arguments)
{
// The compiler turns away an empty list, which fits the lists of triggers and of conditions equally well.
if (script.WaitsOnMixedList(call))
{
return "A wait takes triggers or conditions, not both. List only triggers, or only conditions.";
}
string kind = script.WaitsOnConditions(call) ? "condition" : "trigger";
if (arguments.Count > MaximumWaitedTriggers)
{
return "A wait holds at most " + MaximumWaitedTriggers + " " + kind + "s.";
}
if (arguments.Count == 1 && script.ModelFor(call).GetTypeInfo(arguments[0].Expression).Type
is IArrayTypeSymbol)
{
return "Pass the " + kind + "s one by one, not as an array. The generated code needs to know how " +
"many there are.";
}
return null;
}
// The pause is the wait's own test, so the wait begins once and a resume only asks whether it is over.
private void VisitTriggerWait(StatementSyntax outer, StatementSyntax statement, InvocationExpressionSyntax call,
bool scoped)
{
Location where = call.GetLocation();
if (stepLabel == null)
{
Report(ScriptDiagnostics.UnlabeledPause, where);
return;
}
List triggerArguments = script.TriggerArguments(call);
string problem = TriggerListProblem(call, triggerArguments);
if (problem != null)
{
Report(ScriptDiagnostics.BadTriggerList, where, problem);
return;
}
foreach (ArgumentSyntax argument in call.ArgumentList.Arguments)
{
ReportNestedPauses(argument, "a wait's argument");
}
string key;
string label;
NextName(out key, out label);
if (!ClaimKey(key, where))
{
return;
}
ITypeSymbol type = script.ModelFor(statement).Compilation.GetTypeByMetadataName("Ariadne.TriggerWait");
++triggerWaitCount;
string onConditions = script.WaitsOnConditions(call) ? ", onConditions: true" : "";
var triggers = new SpillTemp("__triggers" + triggerWaitCount, type, where)
{
Saved = true,
Initializer = "new global::Ariadne.TriggerWait(" + triggerArguments.Count + onConditions + ")",
};
Temps.Add(triggers);
// The deadline is kept after the triggers, and a resume tests it rather than making a new one.
ArgumentSyntax timeout = script.TimeoutArgument(call);
SpillTemp deadline = null;
SpillTemp[] liveTemps = { triggers };
if (timeout != null)
{
ITypeSymbol duration = script.ModelFor(statement).Compilation.GetTypeByMetadataName("Amphora.Duration");
++waitCount;
deadline = new SpillTemp("__wait" + waitCount, duration, where) { Saved = true };
Temps.Add(deadline);
liveTemps = new[] { triggers, deadline };
}
var pause = new Pause(PauseKind.Trigger, statement, outer, key, label + "__resume", liveTemps: liveTemps);
Add(PlanPart.Label(label, statement.GetLocation()));
if (deadline != null)
{
Add(PlanPart.Deadline(timeout.Expression, deadline, scoped));
}
Add(PlanPart.TriggerBegin(call, triggers, scoped));
Add(PlanPart.Label(pause.Label, statement.GetLocation()));
AddPause(pause, scoped);
}
private void VisitLoop(StatementSyntax outer, WhileStatementSyntax loop, bool scoped)
{
Location whileLocation = loop.WhileKeyword.GetLocation();
if (stepLabel == null)
{
Report(ScriptDiagnostics.UnlabeledPause, whileLocation);
return;
}
string key;
string label;
NextName(out key, out label);
if (!ClaimKey(key, whileLocation))
{
return;
}
ReportNestedPauses(loop.Condition, "a condition");
SyntaxList inner = ((BlockSyntax)loop.Statement).Statements;
for (int i = 0; i < inner.Count - 1; ++i)
{
ReportNestedPauses(inner[i]);
}
Add(PlanPart.Label(label, whileLocation));
AddPause(new Pause(PauseKind.YieldLoop, loop, outer, key, label), scoped);
}
// The call's own label keys it, so a passage called twice can be told apart; otherwise its name does.
private void VisitCall(StatementSyntax outer, StatementSyntax statement, InvocationExpressionSyntax call,
string ownLabel, bool scoped)
{
IMethodSymbol passage = script.AnyPassageCalled(call);
string key = passage.Name;
string label = key;
if (ownLabel != null)
{
NextName(out key, out label);
}
if (!ClaimKey(key, call.GetLocation()))
{
return;
}
foreach (ArgumentSyntax argument in call.ArgumentList.Arguments)
{
ReportNestedPauses(argument);
}
ForeignSite site = SiteFor(call, key);
Add(PlanPart.Label(label, statement.GetLocation()));
AddPause(new Pause(PauseKind.PassageCall, statement, outer, key, label, foreign: site), scoped);
}
private void PushInOrder(List sequence)
{
for (int i = sequence.Count - 1; i >= 0; --i)
{
pending.Push(sequence[i]);
}
}
private static Work Visiting(StatementSyntax statement, JumpTargets loop)
{
return new Work { Statement = statement, Scoped = true, Loop = loop };
}
private static Work Emitting(PlanPart part)
{
return new Work { Part = part };
}
private void ExpandBlock(BlockSyntax block, JumpTargets loop)
{
var sequence = new List();
foreach (StatementSyntax child in block.Statements)
{
sequence.Add(Visiting(child, loop));
}
PushInOrder(sequence);
}
// The part for a spill step that isn't a call, or null for a call.
private static PlanPart PartFor(SpillStep step, SpillSequence spill, bool scoped, Location anchor)
{
PlanPart returnMe = null;
if (step.Kind == SpillStepKind.Store)
{
returnMe = PlanPart.Store(step.Value, step.Temp, scoped, spill.Replacements);
}
else if (step.Kind == SpillStepKind.Branch)
{
returnMe = PlanPart.Branch(step.Value, step.Negate, step.Name, step.Value.GetLocation(),
step.NullCheck, spill.Replacements);
}
else if (step.Kind == SpillStepKind.ArmTest)
{
returnMe = PlanPart.ArmTest(step.Value, step.Arm, step.Name, spill.Replacements);
}
else if (step.Kind == SpillStepKind.Jump)
{
returnMe = PlanPart.Jump(step.Name, anchor);
}
else if (step.Kind == SpillStepKind.Label)
{
returnMe = PlanPart.Label(step.Name, anchor);
}
return returnMe;
}
// What a call's frame must keep: the call, everything the later steps read, then whatever reads last, if any.
private static List ReadsAfter(SpillSequence spill, int callIndex, SyntaxNode last)
{
var returnMe = new List { spill.Steps[callIndex].Call };
for (int later = callIndex + 1; later < spill.Steps.Count; ++later)
{
SyntaxNode read = spill.Steps[later].Call ?? (SyntaxNode)spill.Steps[later].Value;
if (read != null)
{
returnMe.Add(read);
}
}
if (last != null)
{
returnMe.Add(last);
}
return returnMe;
}
// The steps that take the passage calls out of an if, loop or switch header, to run wherever the header is
// evaluated. Returns what the header should then read from temps, or null if it has no call in it or has
// one that can't be taken out (which is reported). A header call takes the next key of the construct's label,
// so the pauses in its body count on from there.
private IReadOnlyDictionary SpillHeader(List sequence, ExpressionSyntax header,
StatementSyntax owner, string ownLabel, Location anchor, string description = "a condition")
{
SpillSequence spill = ExpressionSpiller.Spill(script, script.ModelFor(header), header, header,
Temps.Count + 1, spillConditionals + 1);
if (spill == null)
{
ReportNestedPauses(header, description);
return null;
}
Temps.AddRange(spill.Temps);
spillConditionals += spill.Conditionals;
for (int i = 0; i < spill.Steps.Count; ++i)
{
SpillStep step = spill.Steps[i];
PlanPart part = PartFor(step, spill, true, anchor);
if (part != null)
{
sequence.Add(Emitting(part));
}
else
{
string key;
string label;
NameSpilledCall(step.Call, ownLabel, out key, out label);
if (!ClaimKey(key, step.Call.GetLocation()))
{
continue;
}
List reads = ReadsAfter(spill, i, header);
var pause = new Pause(PauseKind.PassageCall, owner, owner, key, label, step.Call, step.Temp,
step.LiveTemps, reads, spill.Replacements, owner, SiteFor(step.Call, key));
Pauses.Add(pause);
sequence.Add(Emitting(PlanPart.Label(label, step.Call.GetLocation())));
sequence.Add(Emitting(PlanPart.ForPause(pause, true, spill.Replacements)));
}
}
return spill.Replacements;
}
// if (c) A else B becomes if (!(c)) goto else; A goto end; else: B end:
private void ExpandIf(IfStatementSyntax ifStatement, JumpTargets loop, string ownLabel)
{
++ifCount;
string elseLabel = "__else" + ifCount;
string endLabel = "__end" + ifCount;
Location anchor = ifStatement.IfKeyword.GetLocation();
string target = ifStatement.Else != null ? elseLabel : endLabel;
var sequence = new List();
IReadOnlyDictionary replacements = SpillHeader(sequence, ifStatement.Condition,
ifStatement, ownLabel, anchor);
sequence.Add(Emitting(PlanPart.Branch(ifStatement.Condition, negate: true, target: target,
anchor: anchor, replacements: replacements)));
sequence.Add(Visiting(ifStatement.Statement, loop));
if (ifStatement.Else != null)
{
sequence.Add(Emitting(PlanPart.Jump(endLabel, anchor)));
sequence.Add(Emitting(PlanPart.Label(elseLabel, anchor)));
sequence.Add(Visiting(ifStatement.Else.Statement, loop));
}
sequence.Add(Emitting(PlanPart.Label(endLabel, anchor)));
PushInOrder(sequence);
}
// while (c) A becomes start: if (!(c)) goto end; A goto start; end:
private void ExpandWhile(WhileStatementSyntax loop, JumpTargets outer, string ownLabel)
{
++loopCount;
string start = "__loopStart" + loopCount;
string end = "__loopEnd" + loopCount;
Location anchor = loop.WhileKeyword.GetLocation();
var sequence = new List();
sequence.Add(Emitting(PlanPart.Label(start, anchor)));
IReadOnlyDictionary replacements = SpillHeader(sequence, loop.Condition, loop,
ownLabel, anchor);
sequence.Add(Emitting(PlanPart.Branch(loop.Condition, negate: true, target: end, anchor: anchor,
replacements: replacements)));
sequence.Add(Visiting(loop.Statement, new JumpTargets(end, start, outer?.Cases, outer?.Default)));
sequence.Add(Emitting(PlanPart.Jump(start, anchor)));
sequence.Add(Emitting(PlanPart.Label(end, anchor)));
PushInOrder(sequence);
}
// do A while (c); becomes start: A cont: if (c) goto start; end:
private void ExpandDo(DoStatementSyntax loop, JumpTargets outer, string ownLabel)
{
++loopCount;
string start = "__loopStart" + loopCount;
string cont = "__loopCont" + loopCount;
string end = "__loopEnd" + loopCount;
Location anchor = loop.DoKeyword.GetLocation();
Location whileAnchor = loop.WhileKeyword.GetLocation();
var sequence = new List();
sequence.Add(Emitting(PlanPart.Label(start, anchor)));
sequence.Add(Visiting(loop.Statement, new JumpTargets(end, cont, outer?.Cases, outer?.Default)));
sequence.Add(Emitting(PlanPart.Label(cont, whileAnchor)));
IReadOnlyDictionary replacements = SpillHeader(sequence, loop.Condition, loop,
ownLabel, whileAnchor);
sequence.Add(Emitting(PlanPart.Branch(loop.Condition, negate: false, target: start, anchor: whileAnchor,
replacements: replacements)));
sequence.Add(Emitting(PlanPart.Label(end, whileAnchor)));
PushInOrder(sequence);
}
// for (i; c; s) A becomes i; start: if (!(c)) goto end; A cont: s; goto start; end:
private void ExpandFor(ForStatementSyntax loop, JumpTargets outer, string ownLabel)
{
++loopCount;
string start = "__loopStart" + loopCount;
string cont = "__loopCont" + loopCount;
string end = "__loopEnd" + loopCount;
Location anchor = loop.ForKeyword.GetLocation();
var sequence = new List();
// Initializers run once in order, so a call can only be taken out of a lone one without reordering.
ExpressionSyntax initializer = null;
if (loop.Declaration != null && loop.Declaration.Variables.Count == 1)
{
initializer = loop.Declaration.Variables[0].Initializer?.Value;
}
else if (loop.Declaration == null && loop.Initializers.Count == 1)
{
initializer = loop.Initializers[0];
}
IReadOnlyDictionary initReplacements = null;
if (initializer != null)
{
initReplacements = SpillHeader(sequence, initializer, loop, ownLabel, anchor,
"a for loop's initializer");
}
else
{
if (loop.Declaration != null)
{
ReportNestedPauses(loop.Declaration, "a for loop's initializer");
}
foreach (ExpressionSyntax each in loop.Initializers)
{
ReportNestedPauses(each, "a for loop's initializer");
}
}
bool initializerIsTheCall = loop.Declaration == null && initializer != null && IsPassageCall(initializer);
if ((loop.Declaration != null || loop.Initializers.Count > 0) && !initializerIsTheCall)
{
sequence.Add(Emitting(PlanPart.ForInit(loop, initReplacements)));
}
sequence.Add(Emitting(PlanPart.Label(start, anchor)));
if (loop.Condition != null)
{
IReadOnlyDictionary replacements = SpillHeader(sequence, loop.Condition, loop,
ownLabel, anchor);
sequence.Add(Emitting(PlanPart.Branch(loop.Condition, negate: true, target: end, anchor: anchor,
replacements: replacements)));
}
sequence.Add(Visiting(loop.Statement, new JumpTargets(end, cont, outer?.Cases, outer?.Default)));
sequence.Add(Emitting(PlanPart.Label(cont, anchor)));
if (loop.Incrementors.Any(incrementor => ContainsPassageCall(incrementor)))
{
foreach (ExpressionSyntax incrementor in loop.Incrementors)
{
IReadOnlyDictionary replacements = SpillHeader(sequence, incrementor, loop,
ownLabel, anchor, "a for loop's step");
if (!IsPassageCall(incrementor))
{
sequence.Add(Emitting(PlanPart.Evaluate(incrementor, anchor, replacements)));
}
}
}
else if (loop.Incrementors.Count > 0)
{
sequence.Add(Emitting(PlanPart.ForStep(loop)));
}
sequence.Add(Emitting(PlanPart.Jump(start, anchor)));
sequence.Add(Emitting(PlanPart.Label(end, anchor)));
PushInOrder(sequence);
}
private void ReportCaseLabel(CasePatternSwitchLabelSyntax label)
{
if (label.WhenClause != null)
{
ReportNestedPauses(label.WhenClause.Condition, "a condition");
}
}
// switch (e) { case 1: A default: B } becomes switch (e) { case 1: goto s0; default: goto s1; } s0: A s1: B
// end: The switch that remains only chooses a section, so its labels and guards behave as the user wrote.
private void ExpandSwitch(SwitchStatementSyntax statement, JumpTargets outer, string ownLabel)
{
++switchCount;
string end = "__switchEnd" + switchCount;
SemanticModel model = script.ModelFor(statement);
var names = new List();
var cases = new Dictionary();
string defaultLabel = null;
for (int i = 0; i < statement.Sections.Count; ++i)
{
string name = "__switch" + switchCount + "_" + i;
names.Add(name);
foreach (SwitchLabelSyntax label in statement.Sections[i].Labels)
{
var caseLabel = label as CaseSwitchLabelSyntax;
if (caseLabel != null)
{
cases[model.GetConstantValue(caseLabel.Value).Value ?? NullCase] = name;
}
else if (label is DefaultSwitchLabelSyntax)
{
defaultLabel = name;
}
else
{
ReportCaseLabel((CasePatternSwitchLabelSyntax)label);
}
}
}
Location anchor = statement.SwitchKeyword.GetLocation();
var targets = new JumpTargets(end, outer?.Continue, cases, defaultLabel);
var sequence = new List();
IReadOnlyDictionary replacements = SpillHeader(sequence, statement.Expression,
statement, ownLabel, anchor, "a switch's governing expression");
sequence.Add(Emitting(PlanPart.Dispatch(statement, names, replacements)));
if (defaultLabel == null)
{
sequence.Add(Emitting(PlanPart.Jump(end, anchor)));
}
for (int i = 0; i < statement.Sections.Count; ++i)
{
sequence.Add(Emitting(PlanPart.Label(names[i], statement.Sections[i].Labels[0].GetLocation())));
foreach (StatementSyntax child in statement.Sections[i].Statements)
{
sequence.Add(Visiting(child, targets));
}
}
sequence.Add(Emitting(PlanPart.Label(end, anchor)));
PushInOrder(sequence);
}
// The goto case and goto default statements that belong to the flattened switch around the statement, in
// source order, each with the section label it goes to now. Null where no section matches.
private List CaseJumpsIn(StatementSyntax statement, JumpTargets targets)
{
if (targets?.Cases == null)
{
return null;
}
SemanticModel model = script.ModelFor(statement);
var returnMe = new List();
foreach (GotoStatementSyntax jump in statement.DescendantNodesAndSelf().OfType())
{
bool isCase = jump.IsKind(SyntaxKind.GotoCaseStatement);
if (!isCase && !jump.IsKind(SyntaxKind.GotoDefaultStatement))
{
continue;
}
bool inNestedSwitch = false;
for (SyntaxNode ancestor = jump.Parent; ancestor != null && jump != statement;
ancestor = ancestor.Parent)
{
if (ancestor is SwitchStatementSyntax)
{
inNestedSwitch = true;
break;
}
if (ancestor == statement)
{
break;
}
}
if (inNestedSwitch)
{
continue;
}
string label = targets.Default;
if (isCase)
{
targets.Cases.TryGetValue(model.GetConstantValue(jump.Expression).Value ?? NullCase, out label);
}
returnMe.Add(label);
}
return returnMe.Count > 0 ? returnMe : null;
}
// A while loop whose only pause is the Yield() ending its body is a pause of its own, written as the loop.
private bool IsPauseLoop(WhileStatementSyntax loop)
{
if (ContainsPause(loop.Condition))
{
return false;
}
SyntaxList inner = ((BlockSyntax)loop.Statement).Statements;
for (int i = 0; i < inner.Count - 1; ++i)
{
if (ContainsPause(inner[i]))
{
return false;
}
}
return true;
}
private void ExpandHolder(StatementSyntax statement, JumpTargets loop, string ownLabel)
{
if (statement is IfStatementSyntax)
{
ExpandIf((IfStatementSyntax)statement, loop, ownLabel);
}
else if (statement is WhileStatementSyntax)
{
ExpandWhile((WhileStatementSyntax)statement, loop, ownLabel);
}
else if (statement is DoStatementSyntax)
{
ExpandDo((DoStatementSyntax)statement, loop, ownLabel);
}
else if (statement is ForStatementSyntax)
{
ExpandFor((ForStatementSyntax)statement, loop, ownLabel);
}
else if (statement is SwitchStatementSyntax)
{
ExpandSwitch((SwitchStatementSyntax)statement, loop, ownLabel);
}
else
{
ExpandBlock((BlockSyntax)statement, loop);
}
}
private void Visit(StatementSyntax labeled, bool scoped, JumpTargets loop)
{
string ownLabel = null;
StatementSyntax statement = labeled;
while (statement is LabeledStatementSyntax)
{
var labeledStatement = (LabeledStatementSyntax)statement;
stepLabel = labeledStatement.Identifier.ValueText;
ownLabel = stepLabel;
unplaced.Add(labeledStatement);
flattened.Add(labeledStatement);
statement = labeledStatement.Statement;
}
if (script.IsYieldStatement(statement))
{
VisitYield(labeled, statement, scoped);
return;
}
InvocationExpressionSyntax wait = script.AsSpanWait(statement);
if (wait != null)
{
VisitWait(labeled, statement, wait, ownLabel, scoped);
return;
}
PassageCall triggerWait = script.AsTriggerWait(statement);
if (triggerWait != null)
{
VisitTriggerWait(labeled, statement, triggerWait.Invocation, scoped);
return;
}
WhileStatementSyntax yieldLoop = script.AsYieldLoop(statement);
if (yieldLoop != null && IsPauseLoop(yieldLoop))
{
VisitLoop(labeled, yieldLoop, scoped);
return;
}
PassageCall call = script.AsPassageCall(statement);
if (call != null && !call.Invocation.ArgumentList.DescendantNodes().OfType()
.Any(invocation => script.AnyPassageCalled(invocation) != null))
{
VisitCall(labeled, statement, call.Invocation, ownLabel, scoped);
return;
}
if (TrySpill(labeled, statement, ownLabel, scoped, loop))
{
return;
}
if (holders.Contains(statement))
{
ExpandHolder(statement, loop, ownLabel);
return;
}
ReportUsingDeclaration(statement);
ReportNestedPauses(statement);
Add(PlanPart.Copy(statement, scoped, loop?.Break, loop?.Continue, CaseJumpsIn(statement, loop)));
}
// Adds a stored operand, a test or a pause for each step that takes the passage calls out of a statement.
// Whatever runs after the last step reads from the temps, and readsLast holds it, or null if nothing does.
private void AddSpillSteps(SpillSequence sequence, StatementSyntax outer, StatementSyntax statement,
string ownLabel, bool scoped, SyntaxNode readsLast)
{
Temps.AddRange(sequence.Temps);
spillConditionals += sequence.Conditionals;
Location where = statement.GetLocation();
for (int i = 0; i < sequence.Steps.Count; ++i)
{
SpillStep step = sequence.Steps[i];
PlanPart part = PartFor(step, sequence, scoped, where);
if (part != null)
{
Add(part);
continue;
}
string key;
string label;
NameSpilledCall(step.Call, ownLabel, out key, out label);
if (!ClaimKey(key, step.Call.GetLocation()))
{
continue;
}
List reads = ReadsAfter(sequence, i, readsLast);
Add(PlanPart.Label(label, step.Call.GetLocation()));
AddPause(new Pause(PauseKind.PassageCall, statement, outer, key, label, step.Call, step.Temp,
step.LiveTemps, reads, sequence.Replacements, foreign: SiteFor(step.Call, key)), scoped,
sequence.Replacements);
}
}
// A statement like `Log(prefix + Ask())` becomes store prefix, call Ask, then the statement reading both from
// their temps. Several calls in one statement take the label's key with a count after it.
private bool TrySpill(StatementSyntax outer, StatementSyntax statement, string ownLabel, bool scoped,
JumpTargets loop)
{
IReadOnlyList roots = SpillRoots(statement);
if (roots == null)
{
return false;
}
SpillSequence sequence = ExpressionSpiller.Spill(script, script.ModelFor(statement), statement, roots,
Temps.Count + 1, spillConditionals + 1);
if (sequence == null)
{
return false;
}
// A call that is the whole statement has been made by its pause.
var expression = statement as ExpressionStatementSyntax;
bool hasFinal = expression == null
|| expression.Expression != roots[0]
|| !(roots[0] is InvocationExpressionSyntax)
|| script.AnyPassageCalled((InvocationExpressionSyntax)roots[0]) == null;
AddSpillSteps(sequence, outer, statement, ownLabel, scoped, hasFinal ? statement : null);
if (hasFinal)
{
Add(PlanPart.Copy(statement, scoped, loop?.Break, loop?.Continue, CaseJumpsIn(statement, loop),
sequence.Replacements));
}
return true;
}
// Each run of parts that sit in braces is a segment. A label ends one, since the passage jumps to it.
private void NumberSegments()
{
int segment = -1;
bool inRun = false;
foreach (PlanPart part in Parts)
{
if (part.Scoped)
{
if (!inRun)
{
++segment;
inRun = true;
}
part.Segment = segment;
}
else
{
inRun = false;
}
}
}
///
/// Returns the labeled statement a goto jumps to, or null for a goto case, goto default or one that jumps
/// nowhere.
///
public static LabeledStatementSyntax LabelOf(SemanticModel model, GotoStatementSyntax jump)
{
if (!jump.IsKind(SyntaxKind.GotoStatement) || jump.Expression == null)
{
return null;
}
var label = model.GetSymbolInfo(jump.Expression).Symbol as ILabelSymbol;
if (label == null || label.Locations.Length == 0 || label.Locations[0].SourceTree == null)
{
return null;
}
Location place = label.Locations[0];
return place.SourceTree.GetRoot().FindToken(place.SourceSpan.Start).Parent as LabeledStatementSyntax;
}
private void FindJumps(BlockSyntax body)
{
SemanticModel model = script.ModelFor(body);
foreach (GotoStatementSyntax jump in body.DescendantNodes().OfType())
{
LabeledStatementSyntax target = LabelOf(model, jump);
if (target != null)
{
jumpedTo.Add(target);
}
}
}
// The generated method has one label scope, so a label copied with its statement can't share a name with one
// the plan flattened into it. C# only needed the two to be in sibling blocks.
private void RenameCollidingInnerLabels(BlockSyntax body)
{
var flattenedNames = new HashSet();
foreach (PlanPart part in Parts)
{
if (part.Kind == PartKind.Label)
{
flattenedNames.Add(part.Name);
}
}
var inner = new List();
var used = new HashSet(flattenedNames);
foreach (LabeledStatementSyntax labeled in body.DescendantNodes().OfType())
{
if (!flattened.Contains(labeled))
{
inner.Add(labeled);
used.Add(labeled.Identifier.ValueText);
}
}
foreach (LabeledStatementSyntax labeled in inner)
{
string name = labeled.Identifier.ValueText;
if (!flattenedNames.Contains(name))
{
continue;
}
string baseName = name + "__inner";
string renamed = baseName;
for (int attempt = 1; !used.Add(renamed); ++attempt)
{
renamed = baseName + attempt;
}
LabelNames[labeled] = renamed;
}
}
public static PassagePlan Build(ScriptClass script, IMethodSymbol passage, List diagnostics)
{
var returnMe = new PassagePlan(script, diagnostics, passage.Name);
BlockSyntax body = script.Methods[passage].Body;
if (body == null)
{
return returnMe;
}
returnMe.FindHolders(body);
returnMe.FindJumps(body);
var start = new List();
foreach (StatementSyntax topLevel in body.Statements)
{
start.Add(new Work { Statement = topLevel, Scoped = false });
}
returnMe.PushInOrder(start);
while (returnMe.pending.Count > 0)
{
Work work = returnMe.pending.Pop();
if (work.Part != null)
{
returnMe.Add(work.Part);
}
else
{
returnMe.Visit(work.Statement, work.Scoped, work.Loop);
}
}
returnMe.RenameCollidingInnerLabels(body);
returnMe.NumberSegments();
return returnMe;
}
}
}