using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Text;
namespace Ariadne.Generator
{
///
/// Writes one passage's copy: a dispatch over its pauses, then its statements with each pause lowered into a
/// return. Saved locals live in fields, reached through ref locals of the same names.
///
public class PassageWriter
{
public const string PassageSuffix = "__Passage";
public const string LocalsToSaveSuffix = "__LocalsToSave";
public const string ArgumentSuffix = "__argument";
///
/// Holds the name of the parameter every passage copy takes, which carries its cursor and its locals.
///
public const string FrameName = "__frame";
///
/// Holds the name of the parameter every passage copy takes after its frame, which carries the tick's time.
///
public const string TimeName = "__time";
public const string TimeTypeName = "global::Ariadne.TickTime";
///
/// The name of the parameter an override's copy takes for the frame, typed as the frame of the class that first
/// declared the passage. The copy casts it to its own class's frame.
///
public const string RootFrameName = "__rootFrame";
///
/// The suffix of the frame field that holds the path a virtual passage is running on, or null if it isn't.
///
public const string ActiveSuffix = "__active";
///
/// Holds the name of the nested class an arc class's generated part declares to hold its locals.
///
public const string FrameTypeName = "__Frame";
private readonly ScriptClass script;
private readonly ScriptLocals scriptLocals;
private readonly PassageLocals locals;
private readonly SemanticModel model;
private readonly CodeWriter code;
private readonly CodeWriter cases;
private readonly CodeWriter saveCases;
private readonly bool returnsValue;
// What differs for a passage that owns a frame. Its copy and frame take the passage's type parameters.
private readonly string frameType;
private readonly string typeParameters;
private readonly string constraints;
// The arc's own frame, as this copy reaches it. A passage that owns a frame keeps only its own locals there,
// so it's handed the arc's too, for the passages of the arc that it calls.
private readonly string arcFrame;
private readonly string arcWriter;
private readonly bool ownsFrame;
///
/// Holds the name of the parameter the copy of a passage that owns a frame takes for the arc's own frame.
///
public const string ArcFrameName = "__arcFrame";
///
/// Holds the name of the arc frame's field for the frame of Run, when Run owns one.
///
public const string RunFrameField = "__runFrame";
// Throws before a call goes deeper than the thread's stack allows, so a cycle of calls that never ends fails
// with an exception rather than killing the process.
private const string StackGuard =
"global::System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack(); ";
///
/// Holds the name of the parameter the saving method of a passage that owns a frame takes for the writer the
/// arc's own frame saves to.
///
public const string ArcWriterName = "arcWriter";
///
/// Returns the name of the class that holds the locals of , which owns a frame.
///
public static string OwnedFrameName(IMethodSymbol passage)
{
return passage.Name + FrameTypeName;
}
///
/// Returns 's type parameters as <X, Y>, or "" for one without any.
///
public static string TypeParametersOf(IMethodSymbol passage)
{
if (passage.TypeParameters.Length == 0)
{
return "";
}
return "<" + string.Join(", ", passage.TypeParameters.Select(parameter => parameter.Name)) + ">";
}
///
/// Returns the where clauses of , ready to follow a parameter list, or "".
///
public static string ConstraintsOf(MethodDeclarationSyntax syntax)
{
if (syntax.ConstraintClauses.Count == 0)
{
return "";
}
return " " + string.Join(" ", syntax.ConstraintClauses.Select(clause => clause.ToString()));
}
// Leaves the passage's saved path behind, so calling it again starts it from the top.
private static string FinishText(string copyOut)
{
string exit = FrameName + ".Exit(); return true;";
return copyOut.Length == 0 ? exit : copyOut + " " + exit;
}
private string FinishText()
{
return FinishText(copyOut);
}
// What goes ahead of every return that finishes the passage: handing each out parameter's value to the caller,
// and a virtual passage's marking itself no longer running.
private readonly string copyOut;
// A virtual passage runs on the arc's frame whichever class's copy is called, so two runs at once would share
// locals. Only a call that recurses through an override can start one, which this field turns into an error.
// It holds the path the passage is running on, which a restart replaces.
private readonly bool isVirtual;
private readonly bool isOverride;
private readonly string rootFrameType;
private readonly string running;
// Where the braces of the segment being written sit.
private int segmentColumn;
// The operands of the part being written that are read from temps, and the one among them that is itself
// being written.
private IReadOnlyDictionary replacements;
private SyntaxNode replacementRoot;
private PassageWriter(CodeWriter code, ScriptClass script, ScriptLocals scriptLocals, IMethodSymbol passage)
{
this.code = code;
this.script = script;
this.scriptLocals = scriptLocals;
locals = scriptLocals.ByPassage[passage];
model = script.ModelFor(script.Methods[passage]);
returnsValue = !passage.ReturnsVoid;
typeParameters = TypeParametersOf(passage);
constraints = ConstraintsOf(script.Methods[passage]);
frameType = FrameTypeName;
arcFrame = FrameName;
arcWriter = "writer";
ownsFrame = locals.OwnsFrame;
if (ownsFrame)
{
frameType = OwnedFrameName(passage) + typeParameters;
arcFrame = ArcFrameName;
arcWriter = ArcWriterName;
}
isVirtual = script.IsVirtualPassage(passage) && !ownsFrame;
isOverride = isVirtual && passage.IsOverride;
string frameParameter = isOverride ? RootFrameName : FrameName;
running = frameParameter + "." + passage.Name + ActiveSuffix;
if (isOverride)
{
rootFrameType = ScriptClass.DeclaringMethod(passage).ContainingType
.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + "." + FrameTypeName;
}
IEnumerable finishing = passage.Parameters
.Where(parameter => parameter.RefKind == RefKind.Out)
.Select(parameter => parameter.Name + ArgumentSuffix + " = " + locals.Access(parameter) + ";");
if (isVirtual)
{
finishing = finishing.Concat(new[] { running + " = null;" });
}
copyOut = string.Join(" ", finishing);
// Cases are found while the body is written, so they're kept apart and put into their switch afterward.
cases = new CodeWriter(code.Depth + 2);
saveCases = new CodeWriter(code.Depth + 2);
}
///
/// Rewrites a passage's return; into its copy's return true;, meaning "passage done", and a
/// return value; into the same after handing the value back.
///
private class ReturnRewriter : CSharpSyntaxRewriter
{
private readonly string copyOut;
private readonly string finish;
public ReturnRewriter(string copyOut)
{
this.copyOut = copyOut;
finish = FinishText(copyOut);
}
public override SyntaxNode VisitReturnStatement(ReturnStatementSyntax node)
{
if (node.Expression != null)
{
return SyntaxFactory.ParseStatement("{ __result = " + node.Expression + "; " + finish + " }").WithTriviaFrom(node);
}
return SyntaxFactory.ParseStatement("{ " + finish + " }").WithTriviaFrom(node);
}
public override SyntaxNode VisitParenthesizedLambdaExpression(ParenthesizedLambdaExpressionSyntax node)
{
return node;
}
public override SyntaxNode VisitSimpleLambdaExpression(SimpleLambdaExpressionSyntax node)
{
return node;
}
public override SyntaxNode VisitAnonymousMethodExpression(AnonymousMethodExpressionSyntax node)
{
return node;
}
public override SyntaxNode VisitLocalFunctionStatement(LocalFunctionStatementSyntax node)
{
return node;
}
}
///
/// Writes the labels in a statement under the names they have in the flattened passage, where those aren't
/// what the user wrote. Both lists come one per goto or labeled statement, in source order, with null for one
/// left alone.
///
private class LabelRewriter : CSharpSyntaxRewriter
{
private readonly IReadOnlyList gotoLabels;
private readonly IReadOnlyList labeledNames;
private int nextGoto;
private int nextLabeled;
public LabelRewriter(IReadOnlyList gotoLabels, IReadOnlyList labeledNames)
{
this.gotoLabels = gotoLabels;
this.labeledNames = labeledNames;
}
public override SyntaxNode VisitGotoStatement(GotoStatementSyntax node)
{
string label = gotoLabels[nextGoto];
++nextGoto;
if (label == null)
{
return node;
}
return SyntaxFactory.ParseStatement("goto " + label + ";").WithTriviaFrom(node);
}
public override SyntaxNode VisitLabeledStatement(LabeledStatementSyntax node)
{
string name = labeledNames[nextLabeled];
++nextLabeled;
var visited = (LabeledStatementSyntax)base.VisitLabeledStatement(node);
if (name == null)
{
return visited;
}
return visited.WithIdentifier(SyntaxFactory.Identifier(name).WithTriviaFrom(visited.Identifier));
}
}
///
/// Rewrites a break or continue that belongs to a flattened loop into the goto that does the same. One that
/// belongs to a loop or switch inside the statement is left alone.
///
private class JumpRewriter : CSharpSyntaxRewriter
{
private readonly string breakLabel;
private readonly string continueLabel;
private readonly IReadOnlyList caseJumps;
private int loops;
private int switches;
private int nextCaseJump;
public JumpRewriter(string breakLabel, string continueLabel, IReadOnlyList caseJumps)
{
this.breakLabel = breakLabel;
this.continueLabel = continueLabel;
this.caseJumps = caseJumps;
}
public override SyntaxNode VisitWhileStatement(WhileStatementSyntax node)
{
++loops;
SyntaxNode returnMe = base.VisitWhileStatement(node);
--loops;
return returnMe;
}
public override SyntaxNode VisitDoStatement(DoStatementSyntax node)
{
++loops;
SyntaxNode returnMe = base.VisitDoStatement(node);
--loops;
return returnMe;
}
public override SyntaxNode VisitForStatement(ForStatementSyntax node)
{
++loops;
SyntaxNode returnMe = base.VisitForStatement(node);
--loops;
return returnMe;
}
public override SyntaxNode VisitForEachStatement(ForEachStatementSyntax node)
{
++loops;
SyntaxNode returnMe = base.VisitForEachStatement(node);
--loops;
return returnMe;
}
public override SyntaxNode VisitSwitchStatement(SwitchStatementSyntax node)
{
++switches;
SyntaxNode returnMe = base.VisitSwitchStatement(node);
--switches;
return returnMe;
}
public override SyntaxNode VisitGotoStatement(GotoStatementSyntax node)
{
if (switches > 0
|| caseJumps == null
|| !(node.IsKind(SyntaxKind.GotoCaseStatement) || node.IsKind(SyntaxKind.GotoDefaultStatement)))
{
return node;
}
string label = caseJumps[nextCaseJump++];
if (label == null)
{
return node;
}
return SyntaxFactory.ParseStatement("goto " + label + ";").WithTriviaFrom(node);
}
public override SyntaxNode VisitBreakStatement(BreakStatementSyntax node)
{
if (loops > 0 || switches > 0)
{
return node;
}
return SyntaxFactory.ParseStatement("goto " + breakLabel + ";").WithTriviaFrom(node);
}
public override SyntaxNode VisitContinueStatement(ContinueStatementSyntax node)
{
if (loops > 0)
{
return node;
}
return SyntaxFactory.ParseStatement("goto " + continueLabel + ";").WithTriviaFrom(node);
}
}
///
/// Points a lambda's uses of a hoisted local at its field, since a lambda can't capture the ref local, and turns
/// the declaration of each hoisted pattern or out variable into an assignment to it.
///
private class HoistRewriter : CSharpSyntaxRewriter
{
private readonly ScriptClass script;
private readonly SemanticModel model;
private readonly PassageLocals locals;
// The variables a pattern being rewritten declares, as the hoisted name and the temporary that replaces it.
private readonly Stack>> patterns =
new Stack>>();
private readonly IReadOnlyDictionary replacements;
private readonly SyntaxNode replacementRoot;
public HoistRewriter(ScriptClass script, SemanticModel model, PassageLocals locals,
IReadOnlyDictionary replacements, SyntaxNode replacementRoot)
{
this.script = script;
this.model = model;
this.locals = locals;
this.replacements = replacements;
this.replacementRoot = replacementRoot;
}
// An operand that was stored ahead of a pause is read back from its temp. The operand being stored is
// written as itself.
public override SyntaxNode Visit(SyntaxNode node)
{
SpillTemp temp;
if (node != null && replacements != null && node != replacementRoot
&& replacements.TryGetValue(node, out temp))
{
return SyntaxFactory.ParseExpression(locals.Access(temp)).WithTriviaFrom(node);
}
return base.Visit(node);
}
private bool IsHoisted(SingleVariableDesignationSyntax designation)
{
ISymbol symbol = model.GetDeclaredSymbol(designation);
return symbol != null && locals.IsHoisted(symbol);
}
// A pattern can't assign to an existing variable, so it declares a temporary of its own and the
// hoisted variable is assigned from that once the pattern has matched.
private static string Assignments(List> declared)
{
return string.Join(" && ",
declared.Select(pair => "((" + pair.Key + " = " + pair.Value + ") is var _)"));
}
// The assignments run first, in the when clause, which only runs once the pattern has matched.
private static WhenClauseSyntax AssignmentsWhen(List> declared,
WhenClauseSyntax existing)
{
string condition = Assignments(declared);
if (existing != null)
{
condition += " && (" + existing.Condition + ")";
}
SyntaxToken whenKeyword = SyntaxFactory.Token(SyntaxKind.WhenKeyword)
.WithLeadingTrivia(SyntaxFactory.Space).WithTrailingTrivia(SyntaxFactory.Space);
return SyntaxFactory.WhenClause(whenKeyword, SyntaxFactory.ParseExpression(condition));
}
public override SyntaxNode VisitSingleVariableDesignation(SingleVariableDesignationSyntax node)
{
if (patterns.Count == 0 || !IsHoisted(node))
{
return node;
}
string name = node.Identifier.ValueText;
string temporary = name + "__pattern";
patterns.Peek().Add(new KeyValuePair(name, temporary));
return node.WithIdentifier(SyntaxFactory.Identifier(temporary).WithTriviaFrom(node.Identifier));
}
public override SyntaxNode VisitIsPatternExpression(IsPatternExpressionSyntax node)
{
patterns.Push(new List>());
SyntaxNode visited = base.VisitIsPatternExpression(node);
List> declared = patterns.Pop();
if (declared.Count == 0)
{
return visited;
}
return SyntaxFactory.ParseExpression("(" + visited + " && " + Assignments(declared) + ")")
.WithTriviaFrom(node);
}
public override SyntaxNode VisitCasePatternSwitchLabel(CasePatternSwitchLabelSyntax node)
{
patterns.Push(new List>());
var visited = (CasePatternSwitchLabelSyntax)base.VisitCasePatternSwitchLabel(node);
List> declared = patterns.Pop();
if (declared.Count == 0)
{
return visited;
}
return visited.WithPattern(visited.Pattern.WithoutTrailingTrivia())
.WithWhenClause(AssignmentsWhen(declared, visited.WhenClause));
}
// A compound assignment whose old value was kept before a pause is written out in full, since a temp can't
// take the operator. The cast gives back the implicit conversion that the operator form includes.
public override SyntaxNode VisitAssignmentExpression(AssignmentExpressionSyntax node)
{
SpillTemp old;
ITypeSymbol type = model.GetTypeInfo(node.Left).Type;
if (replacements == null
|| node.IsKind(SyntaxKind.SimpleAssignmentExpression)
|| node.IsKind(SyntaxKind.CoalesceAssignmentExpression)
|| type == null
|| !replacements.TryGetValue(node.Left, out old))
{
return base.VisitAssignmentExpression(node);
}
var target = (ExpressionSyntax)new HoistRewriter(
script, model, locals, replacements, node.Left).Visit(node.Left);
var right = (ExpressionSyntax)Visit(node.Right);
string text = node.OperatorToken.ValueText;
string typeName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
string written = target.ToString().Trim() + " = (" + typeName + ")(" + locals.Access(old) + " "
+ text.Substring(0, text.Length - 1) + " (" + right.ToString().Trim() + "))";
if (node.Parent is ExpressionStatementSyntax || node.Parent is ForStatementSyntax)
{
return SyntaxFactory.ParseExpression(written).WithTriviaFrom(node);
}
return SyntaxFactory.ParseExpression("(" + written + ")").WithTriviaFrom(node);
}
public override SyntaxNode VisitSwitchExpressionArm(SwitchExpressionArmSyntax node)
{
patterns.Push(new List>());
var visited = (SwitchExpressionArmSyntax)base.VisitSwitchExpressionArm(node);
List> declared = patterns.Pop();
if (declared.Count == 0)
{
return visited;
}
// The space keeps the condition off the arrow that follows it.
WhenClauseSyntax clause = AssignmentsWhen(declared, visited.WhenClause);
return visited.WithPattern(visited.Pattern.WithoutTrailingTrivia())
.WithWhenClause(clause.WithCondition(clause.Condition.WithTrailingTrivia(SyntaxFactory.Space)));
}
// "out var n" becomes "out n", and "var (a, b) = pair" becomes "(a, b) = pair", with only the variables
// that aren't hoisted left as declarations.
public override SyntaxNode VisitDeclarationExpression(DeclarationExpressionSyntax node)
{
var single = node.Designation as SingleVariableDesignationSyntax;
if (single != null)
{
if (!IsHoisted(single))
{
return node;
}
return SyntaxFactory.IdentifierName(single.Identifier.ValueText).WithTriviaFrom(node);
}
var parenthesized = node.Designation as ParenthesizedVariableDesignationSyntax;
if (parenthesized == null
|| !parenthesized.DescendantNodes().OfType().Any(IsHoisted))
{
return node;
}
return SyntaxFactory.ParseExpression(TupleText(node.Type, parenthesized)).WithTriviaFrom(node);
}
private string TupleText(TypeSyntax type, ParenthesizedVariableDesignationSyntax root)
{
var text = new StringBuilder();
var pending = new Stack