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(); pending.Push(root); while (pending.Count > 0) { object next = pending.Pop(); var literal = next as string; if (literal != null) { text.Append(literal); continue; } var single = next as SingleVariableDesignationSyntax; if (single != null) { string name = single.Identifier.ValueText; text.Append(IsHoisted(single) ? name : type + " " + name); continue; } var parenthesized = next as ParenthesizedVariableDesignationSyntax; if (parenthesized == null) { text.Append("_"); continue; } text.Append("("); pending.Push(")"); for (int i = parenthesized.Variables.Count - 1; i >= 0; --i) { pending.Push(parenthesized.Variables[i]); if (i > 0) { pending.Push(", "); } } } return text.ToString(); } private static bool IsFunction(SyntaxNode node) { return node is AnonymousFunctionExpressionSyntax || node is LocalFunctionStatementSyntax; } // The tick's time is a parameter of the passage copy, so it needs no field and a lambda can capture it. private ExpressionSyntax TimeAccess(string member, SyntaxNode replaced) { return SyntaxFactory.ParseExpression(TimeName + "." + member).WithTriviaFrom(replaced); } // this.GameTime is replaced whole, since only the name inside it would otherwise become __time.GameTime. public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyntax node) { string time = script.TimeMemberOf(model.GetSymbolInfo(node.Name).Symbol); if (time != null && (node.Expression is ThisExpressionSyntax || node.Expression is BaseExpressionSyntax)) { return TimeAccess(time, node); } return base.VisitMemberAccessExpression(node); } // Runs on the original tree, so each identifier still binds through the semantic model. public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node) { var qualified = node.Parent as MemberAccessExpressionSyntax; string time = script.TimeMemberOf(model.GetSymbolInfo(node).Symbol); if (time != null && (qualified == null || qualified.Name != node)) { return TimeAccess(time, node); } if (!node.Ancestors().Any(IsFunction)) { return node; } ISymbol symbol = model.GetSymbolInfo(node).Symbol; if (symbol == null || !locals.IsHoisted(symbol)) { return node; } return SyntaxFactory.ParseExpression(locals.Access(symbol)).WithTriviaFrom(node); } } // Plain #line maps lines but not columns, and Visual Studio binds a breakpoint only to code starting at or // after its column, so each copied statement starts exactly where the original does. public static void AddMapped(CodeWriter code, Location location, string text) { LinePosition start = location.GetLineSpan().StartLinePosition; code.AddDirective("#line " + (start.Line + 1) + " \"" + location.SourceTree.FilePath + "\""); code.AddLineAt(start.Character, text); } private static void AddHidden(CodeWriter code, string text) { code.AddDirective("#line hidden"); code.AddLine(text); } // Scaffolding lined up with the user's code next to it, so the code-behind reads like ordinary C#. private static void AddHidden(CodeWriter code, string text, Location alignWith) { code.AddDirective("#line hidden"); code.AddLineAt(alignWith.GetLineSpan().StartLinePosition.Character, text); } private static void AddHiddenAt(CodeWriter code, int column, string text) { code.AddDirective("#line hidden"); code.AddLineAt(column, text); } private static string TypeName(ISymbol symbol) { return PassageLocals.TypeOf(symbol).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); } private SyntaxNode Captured(SyntaxNode node) { return new HoistRewriter(script, model, locals, replacements, replacementRoot).Visit(node); } // Null when every label and goto in the statement can stay as written. private LabelRewriter LabelRewriterFor(StatementSyntax statement) { IReadOnlyDictionary names = locals.Plan.LabelNames; if (names.Count == 0) { return null; } var gotoLabels = new List(); var labeledNames = new List(); bool anyChange = false; foreach (SyntaxNode node in statement.DescendantNodesAndSelf()) { var jump = node as GotoStatementSyntax; var labeled = node as LabeledStatementSyntax; string flattened; if (jump != null) { LabeledStatementSyntax target = PassagePlan.LabelOf(model, jump); string name = null; if (target != null && names.TryGetValue(target, out flattened) && flattened != target.Identifier.ValueText) { name = flattened; anyChange = true; } gotoLabels.Add(name); } else if (labeled != null) { string name = null; if (names.TryGetValue(labeled, out flattened) && flattened != labeled.Identifier.ValueText) { name = flattened; anyChange = true; } labeledNames.Add(name); } } if (!anyChange) { return null; } return new LabelRewriter(gotoLabels, labeledNames); } private string WithReturnTrue(StatementSyntax statement, string breakLabel, string continueLabel, IReadOnlyList caseJumps) { SyntaxNode returning = new ReturnRewriter(copyOut).Visit(Captured(statement)); LabelRewriter labelRewriter = LabelRewriterFor(statement); if (labelRewriter != null) { returning = labelRewriter.Visit(returning); } if (breakLabel == null && continueLabel == null) { return returning.ToString(); } return new JumpRewriter(breakLabel, continueLabel, caseJumps).Visit(returning).ToString(); } private string LoopHeader(WhileStatementSyntax loop) { SourceText text = loop.SyntaxTree.GetText(); return text.ToString(TextSpan.FromBounds(loop.WhileKeyword.SpanStart, loop.Condition.SpanStart)) + Captured(loop.Condition) + text.ToString(TextSpan.FromBounds(loop.Condition.Span.End, loop.CloseParenToken.Span.End)); } private void AddCase(string key, string label) { cases.AddLine("case \"" + key + "\": goto " + label + ";"); } // Whether the passage called on this arc has locals to save, though its class may be a base class's. private bool CalleeSaves(IMethodSymbol callee) { ScriptClass calleeScript = script.OwnerOf(callee); if (calleeScript == script) { return scriptLocals.Saving.Contains(callee); } return script.Registry.LocalsOf(calleeScript, new List()).Saving.Contains(callee); } // The callee's locals go in a table of their own, named for the call. The frame is lent first because one // loaded from a save hasn't been yet, and has no path of its own to read. private void WriteForeignSaveCase(ForeignSite site) { ScriptLocals ownerLocals = scriptLocals; if (site.Owner != script) { ownerLocals = script.Registry.LocalsOf(site.Owner, new List()); } bool calleeSaves = ownerLocals.Saving.Contains(site.Passage); if (site.OnThisArc && !calleeSaves) { return; } string scope = "global::Ariadne.IArcWriter scope = writer.Scope(\"" + site.ScopeName + "\");"; string lent = FrameName + ".Lend(" + site.TypedFrameAccess + ")"; string callee = site.Passage.Name + LocalsToSaveSuffix + site.TypeArguments; if (site.IsBaseCall) { saveCases.AddLine("{ " + scope + " base." + callee + "(" + FrameName + ".Lend(" + site.FrameAccess + "), depth + 1, scope); }"); return; } // The arc's frame holds the locals of the arc's passages that a passage with its own frame calls. They // stay where the arc's own are, rather than going in the call's table. if (site.OnThisArc) { string typeArguments = ""; if (site.Polymorphic) { typeArguments = " scope.TypeArguments(" + site.TypeArgumentTypes + ");"; } saveCases.AddLine("{ " + scope + typeArguments + " " + callee + "(" + lent + ", " + arcFrame + ", depth + 1, scope, " + arcWriter + "); }"); return; } // A passage on another arc has a receiver to save, whether or not it has locals. string receiver = FrameName + ".SaveLocal(\"" + site.ReceiverKey + "\", scope);"; if (!calleeSaves) { saveCases.AddLine("{ " + scope + " " + receiver + " }"); return; } // The arc it runs on keeps its own passages' locals apart from the callee's own frame, though both are // in the call's table. string writers = "scope"; if (site.HasOwnedFrame) { lent = FrameName + ".Lend(" + site.OwnedFrameAccess + "), " + lent; writers = "scope, scope"; } // A save missing the receiver is reported by VerifyLoaded, which saves into a shape to find out. saveCases.AddLine("{ " + scope + " " + receiver + " if (" + site.ReceiverAccess + " != null) { " + site.ReceiverAccess + "." + callee + "(" + lent + ", depth + 1, " + writers + "); } }"); } private void AddSaveCase(Pause pause, IMethodSymbol callee) { List needed; locals.NeededAt.TryGetValue(pause, out needed); ForeignSite site = pause.Foreign; bool calleeSaves = site == null && callee != null && CalleeSaves(callee); if ((needed == null || needed.Count == 0) && pause.LiveTemps.Count == 0 && !calleeSaves && site == null) { return; } saveCases.AddLine("case \"" + pause.Key + "\":"); saveCases.Indent(); foreach (ISymbol symbol in needed ?? Enumerable.Empty()) { saveCases.AddLine(FrameName + ".SaveLocal(\"" + locals.Key(symbol) + "\", writer);"); } foreach (SpillTemp temp in pause.LiveTemps) { saveCases.AddLine(FrameName + ".SaveLocal(\"" + locals.Key(temp) + "\", writer);"); } if (calleeSaves) { saveCases.AddLine(callee.Name + LocalsToSaveSuffix + "(" + arcFrame + ", depth + 1, " + arcWriter + ");"); } if (site != null) { WriteForeignSaveCase(site); } saveCases.AddLine("break;"); saveCases.Dedent(); } // A hoisted local is declared as a ref to its field, so its own declaration becomes an assignment. // Returns null if the declaration declares nothing hoisted. private string HoistedDeclaration(VariableDeclarationSyntax declaration) { bool hoistsAny = false; var parts = new List(); foreach (VariableDeclaratorSyntax declarator in declaration.Variables) { if (!locals.IsHoisted(model.GetDeclaredSymbol(declarator))) { parts.Add(declaration.Type + " " + Captured(declarator) + ";"); continue; } hoistsAny = true; if (declarator.Initializer != null) { parts.Add(declarator.Identifier.ValueText + " = " + Captured(declarator.Initializer.Value) + ";"); } } if (!hoistsAny) { return null; } return string.Join(" ", parts); } private void WriteStatement(StatementSyntax statement, string breakLabel = null, string continueLabel = null, IReadOnlyList caseJumps = null) { string text = null; var declaration = statement as LocalDeclarationStatementSyntax; if (declaration != null) { text = HoistedDeclaration(declaration.Declaration); } text = text ?? WithReturnTrue(statement, breakLabel, continueLabel, caseJumps); if (text.Length > 0) { AddMapped(code, statement.GetLocation(), text); } } // Everything but the closing Yield() is copied as is; that Yield becomes "record this key, then pause". private void WriteLoopBody(WhileStatementSyntax loop, string key) { SyntaxList inner = ((BlockSyntax)loop.Statement).Statements; for (int i = 0; i < inner.Count - 1; ++i) { WriteStatement(inner[i]); } AddMapped(code, inner.Last().GetLocation(), FrameName + ".Mark(\"" + key + "\"); return false;"); } private void WriteYieldLoop(WhileStatementSyntax loop, string key) { Location whileLocation = loop.WhileKeyword.GetLocation(); AddMapped(code, whileLocation, LoopHeader(loop)); AddHidden(code, "{", whileLocation); WriteLoopBody(loop, key); AddHidden(code, "}", whileLocation); } // A by-value parameter's argument is evaluated only on the way in. Resuming passes a default instead. The // callee takes the saved value if it needs one, and the argument could have side effects or have changed. // A ref, ref readonly or out argument is a variable the caller passes afresh each tick, and the context is // handed to every tick, so those are passed as written. // The arguments of a params call that no other parameter takes make one array, which resuming leaves out // rather than build again. private string Arguments(InvocationExpressionSyntax call, IMethodSymbol passage, string resultArgument) { // The callee's own analysis says which of its parameters it renames, and what context it takes. ScriptClass calleeScript = script.OwnerOf(passage); ScriptLocals calleeScriptLocals = scriptLocals; if (calleeScript != script) { calleeScriptLocals = script.Registry.LocalsOf(calleeScript, new List()); } PassageLocals calleeLocals = null; if (!calleeScript.FromMetadata) { calleeLocals = calleeScriptLocals.ByPassage[passage]; } var invocation = model.GetOperation(call) as IInvocationOperation; var parts = new List(); var arrayElements = new List(); IArgumentOperation paramArray = invocation?.Arguments .FirstOrDefault(operation => operation.ArgumentKind == ArgumentKind.ParamArray); foreach (ArgumentSyntax argument in call.ArgumentList.Arguments) { // The analysis knows what the parameter the callee's class declares. A call on a constructed generic // class needs the parameter's type as constructed, since the caller has no T. IParameterSymbol constructed = invocation?.Arguments .FirstOrDefault(operation => operation.Syntax == argument)?.Parameter; IParameterSymbol parameter = constructed?.OriginalDefinition; if (parameter == null && paramArray != null) { arrayElements.Add(Captured(argument.Expression).ToString()); continue; } if (parameter == null || parameter.RefKind == RefKind.Ref || parameter.RefKind == RefKind.RefReadOnlyParameter || parameter.RefKind == RefKind.Out || SymbolEqualityComparer.Default.Equals(parameter.Type, calleeScript.ContextType)) { parts.Add(Captured(argument).ToString()); continue; } string name = ""; if (argument.NameColon != null) { bool renamed = calleeScript.FromMetadata ? calleeScript.RenamesParameter(passage, parameter) : calleeLocals.IsSaved(parameter); name = parameter.Name + (renamed ? ArgumentSuffix : "") + ": "; } parts.Add(name + FrameName + ".ResumeKey == null ? (" + Captured(argument.Expression) + ") : default(" + TypeName(constructed) + ")"); } if (arrayElements.Count > 0) { var arrayType = (IArrayTypeSymbol)paramArray.Parameter.Type; string elementType = arrayType.ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); parts.Add(FrameName + ".ResumeKey == null ? new " + elementType + "[] { " + string.Join(", ", arrayElements) + " } : null"); } if (resultArgument != null) { parts.Add(resultArgument); } return string.Join(", ", parts); } // Each label keeps its own line, so a breakpoint on a case hits where the user put it. private void WriteDispatch(SwitchStatementSyntax statement, IReadOnlyList sectionLabels) { Location switchLocation = statement.SwitchKeyword.GetLocation(); AddMapped(code, switchLocation, "switch (" + Captured(statement.Expression) + ")"); AddHidden(code, "{", switchLocation); for (int i = 0; i < statement.Sections.Count; ++i) { SyntaxList labels = statement.Sections[i].Labels; for (int j = 0; j < labels.Count; ++j) { string text = Captured(labels[j]).ToString(); if (j == labels.Count - 1) { text += " goto " + sectionLabels[i] + ";"; } AddMapped(code, labels[j].GetLocation(), text); } } AddHidden(code, "}", switchLocation); } private void WriteForInit(ForStatementSyntax loop) { var parts = new List(); if (loop.Declaration != null) { parts.Add(HoistedDeclaration(loop.Declaration) ?? Captured(loop.Declaration) + ";"); } foreach (ExpressionSyntax initializer in loop.Initializers) { parts.Add(Captured(initializer) + ";"); } AddMapped(code, loop.ForKeyword.GetLocation(), string.Join(" ", parts)); } private void WriteForStep(ForStatementSyntax loop) { AddMapped(code, loop.ForKeyword.GetLocation(), string.Join(" ", loop.Incrementors.Select(incrementor => Captured(incrementor) + ";"))); } // The callee hands its value back through an out parameter, which has to be of exactly its type, so the value // lands in a temporary first and goes on from there. The named argument keeps it last whatever the user // named. private string EnterText(Pause pause, InvocationExpressionSyntax call, IMethodSymbol passage, string resultArgument) { string arguments = Arguments(call, passage, resultArgument); if (arguments.Length > 0) { arguments = ", " + arguments; } ForeignSite site = pause.Foreign; if (site == null && !ownsFrame) { return "using (" + FrameName + ".Enter(\"" + pause.Key + "\")) { if (!" + passage.Name + PassageSuffix + "(" + FrameName + ", " + TimeName + arguments + ")) { return false; } }"; } // A passage with its own frame runs a passage of the arc on the arc's frame, which takes this passage's // place on the path for the call and gets back the depth it had, since the passage that called this one is // still running on it. if (site == null) { return "using (" + FrameName + ".Enter(\"" + pause.Key + "\")) { int __depth = " + ArcFrameName + ".Depth; bool __done = " + passage.Name + PassageSuffix + "(" + FrameName + ".Lend(" + ArcFrameName + "), " + TimeName + arguments + "); " + ArcFrameName + ".Depth = __depth; " + "if (!__done) { return false; } }"; } // The base class's Run, which the derived class's method of the same name would otherwise be called for. if (site.IsBaseCall) { return "using (" + FrameName + ".Enter(\"" + pause.Key + "\")) { if (" + site.FrameAccess + " == null) { " + site.FrameAccess + " = new " + site.FrameType + "(); } if (!base." + passage.Name + PassageSuffix + "(" + FrameName + ".Lend(" + site.FrameAccess + "), " + TimeName + arguments + ")) { return false; } }"; } // The callee keeps its locals in a frame of its own, made on the first call from this site. if (site.OnThisArc) { string guard = ""; if (script.InSameCycle(locals.Passage, passage)) { guard = StackGuard; } return "using (" + FrameName + ".Enter(\"" + pause.Key + "\")) { " + guard + "if (" + site.FrameAccess + " == null) { " + site.FrameAccess + " = new " + site.FrameType + "(); } if (!" + passage.Name + PassageSuffix + site.TypeArguments + "(" + FrameName + ".Lend(" + site.TypedFrameAccess + "), " + arcFrame + ", " + TimeName + arguments + ")) { return false; } }"; } // The receiver and a frame for the callee are made when the call begins and kept in this arc's frame, so // resuming, or loading a save, finds them again. A callee with its own frame runs with that as well, and // is handed the receiver's frame for the passages of that arc that it calls. string forget = ""; string make = ""; string lent = FrameName + ".Lend(" + site.FrameAccess + ")"; string typeArguments = ""; if (site.HasOwnedFrame) { forget = site.OwnedFrameAccess + " = null; "; make = " if (" + site.OwnedFrameAccess + " == null) { " + site.OwnedFrameAccess + " = new " + site.OwnedFrameType + "(); }"; lent = FrameName + ".Lend(" + site.OwnedFrameAccess + "), " + lent; typeArguments = site.TypeArguments; } return "using (" + FrameName + ".Enter(\"" + pause.Key + "\")) { " + StackGuard + "if (" + FrameName + ".ResumeKey == null) { " + site.ReceiverAccess + " = (" + Captured(site.Receiver) + "); " + site.FrameAccess + " = null; " + forget + "} if (" + site.FrameAccess + " == null) { " + site.FrameAccess + " = (" + site.FrameType + ")" + site.ReceiverAccess + ".NewFrame(); }" + make + " if (!" + site.ReceiverAccess + "." + passage.Name + PassageSuffix + typeArguments + "(" + lent + ", " + TimeName + arguments + ")) { return false; } }"; } // The type the call hands back as constructed at the call, which differs from the passage's own for a generic // passage or a passage of a generic class. private string ReturnTypeOf(InvocationExpressionSyntax call) { var target = (IMethodSymbol)model.GetSymbolInfo(call).Symbol; return target.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); } // A call inside a bigger statement leaves its value in a temp for that statement to read. private void WriteSpilledCall(Pause pause) { IMethodSymbol passage = script.AnyPassageCalled(pause.Call); if (passage.ReturnsVoid) { AddMapped(code, pause.Call.GetLocation(), EnterText(pause, pause.Call, passage, null)); return; } string type = ReturnTypeOf(pause.Call); AddMapped(code, pause.Call.GetLocation(), "{ " + type + " __value; " + EnterText(pause, pause.Call, passage, "__result: out __value") + " " + locals.Access(pause.Temp) + " = __value; }"); } private void WritePassageCall(Pause pause) { if (pause.Call != null) { WriteSpilledCall(pause); return; } PassageCall call = script.AsPassageCall(pause.Statement); IMethodSymbol passage = script.AnyPassageCalled(call.Invocation); string text; if (passage.ReturnsVoid) { text = EnterText(pause, call.Invocation, passage, null); } else { string type = ReturnTypeOf(call.Invocation); if (call.Kind == PassageCallKind.Discard) { text = EnterText(pause, call.Invocation, passage, "__result: out _"); } else { string enter = "{ " + type + " __value; " + EnterText(pause, call.Invocation, passage, "__result: out __value"); if (call.Kind == PassageCallKind.Assign) { text = enter + " " + Captured(call.Target) + " = __value; }"; } else if (call.Kind == PassageCallKind.Declare) { ISymbol local = model.GetDeclaredSymbol(call.Declarator); string declaration = locals.IsHoisted(local) ? "" : TypeName(local) + " " + local.Name + "; "; text = declaration + enter + " " + local.Name + " = __value; }"; } else { text = enter + " __result = __value; " + FinishText() + " }"; } } } AddMapped(code, pause.Statement.GetLocation(), text); } // The wait ends itself, so the pause only asks. Past the test, the trigger it ended on goes where the // statement puts a passage call's value. A wait that ran out of time has no trigger, which reads as null. private void WriteTriggerPause(Pause pause) { string wait = locals.Access(pause.LiveTemps[0]); string suspend = FrameName + ".Suspend(); " + FrameName + ".Mark(\"" + pause.Key + "\"); return false;"; string text; if (pause.LiveTemps.Count == 1) { text = "if (!" + wait + ".Complete) { " + suspend + " }"; } else { // Sleeping as well as suspended is how the scheduler learns the pause has a timeout. string deadline = locals.Access(pause.LiveTemps[1]); text = "if (!" + wait + ".Complete) { if (" + TimeName + ".GameTime < " + deadline + ") { " + FrameName + ".Sleep(" + deadline + "); " + suspend + " } " + wait + ".Expire(); }"; } // A wait on conditions stays subscribed until it is finished, so even a call that discards its value has to // end it. PassageCall call = script.AsTriggerWait(pause.Statement); bool onConditions = script.WaitsOnConditions(call.Invocation); string result = wait + (onConditions ? ".Finish()" : ".Result"); if (call.Kind == PassageCallKind.Discard && onConditions) { text += " " + result + ";"; } else if (call.Kind == PassageCallKind.Assign) { text += " " + Captured(call.Target) + " = " + result + ";"; } else if (call.Kind == PassageCallKind.Declare) { ISymbol local = model.GetDeclaredSymbol(call.Declarator); string declaration = locals.IsHoisted(local) ? "" : TypeName(local) + " " + local.Name + "; "; text += " " + declaration + local.Name + " = " + result + ";"; } else if (call.Kind == PassageCallKind.Return) { text += " __result = " + result + "; " + FinishText(); } AddMapped(code, pause.Statement.GetLocation(), text); } private void WritePause(Pause pause) { if (pause.Kind == PauseKind.Yield) { AddMapped(code, pause.Statement.GetLocation(), FrameName + ".Mark(\"" + pause.Key + "\"); return false;"); } else if (pause.Kind == PauseKind.YieldLoop) { WriteYieldLoop((WhileStatementSyntax)pause.Statement, pause.Key); } else if (pause.Kind == PauseKind.Wait) { string deadline = locals.Access(pause.LiveTemps[0]); AddMapped(code, pause.Statement.GetLocation(), "if (" + TimeName + ".GameTime < " + deadline + ") { " + FrameName + ".Sleep(" + deadline + "); " + FrameName + ".Mark(\"" + pause.Key + "\"); return false; }"); } else if (pause.Kind == PauseKind.Trigger) { WriteTriggerPause(pause); } else { WritePassageCall(pause); } } // The test that a value matches a switch expression arm, written as an is expression. A discard pattern // without a guard matches anything. private string ArmTest(PlanPart part) { var arm = (SwitchExpressionArmSyntax)new HoistRewriter( script, model, locals, replacements, replacementRoot).Visit(part.Arm); string guard = arm.WhenClause == null ? null : arm.WhenClause.Condition.ToString().Trim(); if (arm.Pattern is DiscardPatternSyntax) { return guard ?? "true"; } string test = "(" + Captured(part.Condition) + ") is " + arm.Pattern.ToString().Trim(); if (guard != null) { test += " && (" + guard + ")"; } return test; } private void WriteTriggerBegin(PlanPart part) { var call = (InvocationExpressionSyntax)part.Condition; string wait = locals.Access(part.Temp); string requiresAll = script.WaitsForAll(call) ? "true" : "false"; string text = wait + ".Begin(" + FrameName + ", requiresAll: " + requiresAll + ");"; foreach (ArgumentSyntax argument in script.TriggerArguments(call)) { text += " " + wait + ".Add(" + Captured(argument.Expression) + ");"; } AddMapped(code, part.Anchor, text); } private void WritePart(PlanPart part) { replacements = part.Replacements; replacementRoot = part.Value; WritePartText(part); replacements = null; replacementRoot = null; } private void WritePartText(PlanPart part) { if (part.Kind == PartKind.Store) { AddMapped(code, part.Anchor, locals.Access(part.Temp) + " = " + Captured(part.Value) + ";"); } else if (part.Kind == PartKind.Deadline) { AddMapped(code, part.Anchor, locals.Access(part.Temp) + " = " + TimeName + ".DeadlineAfter(" + Captured(part.Condition) + ");"); } else if (part.Kind == PartKind.TriggerBegin) { WriteTriggerBegin(part); } else if (part.Kind == PartKind.Evaluate) { AddMapped(code, part.Anchor, Captured(part.Value) + ";"); } else if (part.Kind == PartKind.Copy) { WriteStatement(part.Statement, part.BreakLabel, part.ContinueLabel, part.CaseJumps); } else if (part.Kind == PartKind.Dispatch) { WriteDispatch((SwitchStatementSyntax)part.Statement, part.SectionLabels); } else if (part.Kind == PartKind.Branch) { string condition = Captured(part.Condition).ToString(); if (part.NullCheck == NullCheck.IsNull) { condition = "(" + condition + ") == null"; } else if (part.NullCheck == NullCheck.IsNotNull) { condition = "(" + condition + ") != null"; } if (part.Negate) { condition = "!(" + condition + ")"; } AddMapped(code, part.Anchor, "if (" + condition + ") goto " + part.Name + ";"); } else if (part.Kind == PartKind.ArmTest) { AddMapped(code, part.Anchor, "if (!(" + ArmTest(part) + ")) goto " + part.Name + ";"); } else if (part.Kind == PartKind.ForInit) { WriteForInit((ForStatementSyntax)part.Statement); } else if (part.Kind == PartKind.ForStep) { WriteForStep((ForStatementSyntax)part.Statement); } else if (part.Kind == PartKind.Jump) { AddHidden(code, "goto " + part.Name + ";", part.Anchor); } else if (part.Kind == PartKind.Label) { AddHidden(code, part.Name + ":", part.Anchor); } else { WritePause(part.Pause); } } // Braces keep the user's own names for locals that live in one segment, since sibling branches may each // declare the same name. The braces sit one indent out from the code, as they would in the source. private void OpenSegment(PlanPart first) { int column = first.Anchor.GetLineSpan().StartLinePosition.Character; segmentColumn = Math.Max(0, column - 4); AddHiddenAt(code, segmentColumn, "{"); foreach (ISymbol symbol in locals.AliasesIn(first.Segment)) { code.AddLineAt(column, "ref " + TypeName(symbol) + " " + symbol.Name + " = ref " + locals.Access(symbol) + ";"); } } private void CloseSegment() { AddHiddenAt(code, segmentColumn, "}"); } private void WriteParts() { int open = -1; foreach (PlanPart part in locals.Plan.Parts) { if (open >= 0 && part.Segment != open) { CloseSegment(); open = -1; } if (part.Segment >= 0 && open < 0) { OpenSegment(part); open = part.Segment; } WritePart(part); } if (open >= 0) { CloseSegment(); } } private void WriteDispatchCases() { foreach (Pause pause in locals.Plan.Pauses) { AddCase(pause.Key, pause.Label); IMethodSymbol callee = null; if (pause.Kind == PauseKind.PassageCall) { callee = script.AnyPassageCalled(pause.Call ?? script.AsPassageCall(pause.Statement).Invocation); } AddSaveCase(pause, callee); } } // The frame parameter of the passage's copy and of its saving method. private string FrameParameter() { if (isOverride) { return rootFrameType + " " + RootFrameName; } return frameType + " " + FrameName; } // What makes a copy dispatch on the arc's type, which an override must repeat to override the copy it follows. private string DispatchModifiers(IMethodSymbol method) { if (!isVirtual) { return ""; } if (isOverride) { if (method.IsAbstract) { return " abstract override"; } return method.IsSealed ? " sealed override" : " override"; } return method.IsAbstract ? " abstract" : " virtual"; } // An override takes the frame its root declared, so it casts the frame to the one its own class has. private void WriteFrameCast() { if (isOverride) { code.AddLine("var " + FrameName + " = (" + FrameTypeName + ")" + RootFrameName + ";"); } } // A virtual passage that starts while it is still running has been called back through an override. private void WriteRunningGuard(IMethodSymbol method) { if (!isVirtual) { return; } code.OpenNewBlock("if (" + FrameName + ".ResumeKey == null && object.ReferenceEquals(" + running + ", " + FrameName + ".Cursor))"); code.AddLine("throw new global::System.InvalidOperationException(\"The virtual passage " + method.Name + " started again before it finished. A virtual passage can't be called from itself, even through " + "an override.\");"); code.CloseBlock(); code.NewLine(); code.AddLine(running + " = " + FrameName + ".Cursor;"); } // A saved parameter's name goes to its ref local, so the parameter itself takes a suffix. private string ParameterList(IMethodSymbol method, MethodDeclarationSyntax syntax) { IEnumerable saved = syntax.ParameterList.Parameters .Where(parameter => locals.IsSaved(model.GetDeclaredSymbol(parameter)) || model.GetDeclaredSymbol(parameter).RefKind == RefKind.Out); string written = syntax.ParameterList.ReplaceNodes(saved, (original, rewritten) => rewritten.WithIdentifier(SyntaxFactory.Identifier(original.Identifier.ValueText + ArgumentSuffix))) .ToString(); // The frame and the time go first, and the result last, so none disturbs the user's own parameters. string returnMe = "(" + FrameParameter(); if (ownsFrame) { returnMe += ", " + FrameTypeName + " " + ArcFrameName; } returnMe += ", " + TimeTypeName + " " + TimeName; if (syntax.ParameterList.Parameters.Count > 0) { returnMe += ", " + written.Substring(1, written.Length - 2); } if (!method.ReturnsVoid) { returnMe += ", out " + method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + " __result"; } return returnMe + ")"; } // Declared before the dispatch, since C# won't jump past a ref local's declaration. private void WriteTopAliases() { foreach (ISymbol symbol in locals.TopAliases) { code.AddLine("ref " + TypeName(symbol) + " " + symbol.Name + " = ref " + locals.Access(symbol) + ";"); if (symbol is IParameterSymbol) { code.OpenNewBlock("if (" + FrameName + ".ResumeKey == null)"); if (((IParameterSymbol)symbol).RefKind == RefKind.Out) { code.AddLine(symbol.Name + " = default(" + TypeName(symbol) + ");"); } else { code.AddLine(symbol.Name + " = " + symbol.Name + ArgumentSuffix + ";"); } code.CloseBlock(); code.NewLine(); } } } private void WriteLocalsToSave(IMethodSymbol method) { AddHiddenAttribute(code); string arcFrameParameter = ""; string arcWriterParameter = ""; if (ownsFrame) { arcFrameParameter = ", " + FrameTypeName + " " + ArcFrameName; arcWriterParameter = ", global::Ariadne.IArcWriter " + ArcWriterName; } code.OpenNewBlock(AccessOf(method) + DispatchModifiers(method) + " void " + method.Name + LocalsToSaveSuffix + typeParameters + "(" + FrameParameter() + arcFrameParameter + ", int depth, global::Ariadne.IArcWriter writer" + arcWriterParameter + ")" + constraints); WriteFrameCast(); code.OpenNewBlock("if (depth >= " + FrameName + ".Cursor.Count)"); code.AddLine("return;"); code.CloseBlock(); code.NewLine(); code.OpenNewBlock("switch (" + FrameName + ".Cursor[depth])"); code.AddLines(saveCases); code.CloseBlock(); code.CloseBlock(); code.NewLine(); } // A passage's copy is as visible as the passage is, so another arc can reach it exactly when it could call the // passage. It couldn't be more visible anyway, since the parameter types might not be. private static string AccessOf(IMethodSymbol method) { switch (method.DeclaredAccessibility) { case Accessibility.Public: return "public"; case Accessibility.Internal: return "internal"; case Accessibility.Protected: return "protected"; case Accessibility.ProtectedOrInternal: return "protected internal"; case Accessibility.ProtectedAndInternal: return "private protected"; default: return "private"; } } private static void AddHiddenAttribute(CodeWriter code) { code.AddLine("[global::System.ComponentModel.EditorBrowsable(" + "global::System.ComponentModel.EditorBrowsableState.Never)]"); } // Returns where the dispatch's cases go, which isn't known until the statements have been written. private int WriteMethodStart(IMethodSymbol method, MethodDeclarationSyntax syntax) { AddHiddenAttribute(code); // A class built from this assembly can't read the body, so the copy says whether it reaches such a call. string reachedRun = ScriptChecks.RunByNameReachedFrom(script, method); if (reachedRun != null) { code.AddLine("[global::Ariadne.ReachesRunByName(\"" + reachedRun + "\")]"); } code.OpenNewBlock(AccessOf(method) + DispatchModifiers(method) + " bool " + method.Name + PassageSuffix + typeParameters + ParameterList(method, syntax) + constraints); code.AddDirective("#line hidden"); WriteFrameCast(); WriteRunningGuard(method); if (returnsValue) { code.AddLine("__result = default(" + method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + ");"); } foreach (IParameterSymbol parameter in method.Parameters.Where(each => each.RefKind == RefKind.Out)) { code.AddLine(parameter.Name + ArgumentSuffix + " = default(" + TypeName(parameter) + ");"); } WriteTopAliases(); code.OpenNewBlock("switch (" + FrameName + ".ResumeKey)"); int casesAt = code.Length; code.CloseBlock(); return casesAt; } private void WriteMethodEnd(IMethodSymbol method, int casesAt) { code.InsertLines(casesAt, cases); AddHidden(code, FinishText()); code.AddDirective("#line default"); code.CloseBlock(); code.NewLine(); if (scriptLocals.Saving.Contains(method)) { WriteLocalsToSave(method); } } // An abstract passage has nothing to run, but its overrides need the copy and the saving method to override. private void WriteAbstract(IMethodSymbol method, MethodDeclarationSyntax syntax) { AddHiddenAttribute(code); code.AddLine(AccessOf(method) + DispatchModifiers(method) + " bool " + method.Name + PassageSuffix + typeParameters + ParameterList(method, syntax) + constraints + ";"); code.NewLine(); AddHiddenAttribute(code); code.AddLine(AccessOf(method) + DispatchModifiers(method) + " void " + method.Name + LocalsToSaveSuffix + typeParameters + "(" + FrameParameter() + ", int depth, global::Ariadne.IArcWriter writer)" + constraints + ";"); code.NewLine(); } /// /// Writes the copy of that runs, bool Name__Passage(…), returning true once /// the passage is done. It's followed by Name__LocalsToSave if the passage has locals to save. /// public static void Write(CodeWriter code, ScriptClass script, ScriptLocals scriptLocals, IMethodSymbol method) { MethodDeclarationSyntax syntax = script.Methods[method]; var writer = new PassageWriter(code, script, scriptLocals, method); if (syntax.Body == null) { writer.WriteAbstract(method, syntax); return; } int casesAt = writer.WriteMethodStart(method, syntax); writer.WriteDispatchCases(); writer.WriteParts(); writer.WriteMethodEnd(method, casesAt); } } }