using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Ariadne.Generator { public class CodeBehindWriter { private readonly GeneratedArc arc; private readonly List diagnostics; public CodeBehindWriter(GeneratedArc arc, List diagnostics) { this.arc = arc; this.diagnostics = diagnostics; } private static IEnumerable UsingsFor(SyntaxNode declaration, bool insideNamespace) { if (insideNamespace) { return declaration.Ancestors().OfType() .SelectMany(space => space.Usings).Select(directive => directive.ToString()); } return ((CompilationUnitSyntax)declaration.SyntaxTree.GetRoot()).Usings .Select(directive => directive.ToString()); } private static List DeclarationsOf(GeneratedArc arc) { return arc.Type.DeclaringSyntaxReferences.Select(reference => reference.GetSyntax()).ToList(); } private List Declarations() { return DeclarationsOf(arc); } private static void WriteResume(CodeWriter code, ScriptClass script, ScriptLocals scriptLocals) { string contextType = script.ArcBase.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); code.AddLine("[global::System.CodeDom.Compiler.GeneratedCode(\"" + ScriptClass.GeneratorName + "\", \"1.0\")]"); code.OpenNewBlock("protected override bool __Resume(" + contextType + " context, " + PassageWriter.TimeTypeName + " " + PassageWriter.TimeName + ")"); if (scriptLocals.OwnedRun == null) { code.AddLine("return Run" + PassageWriter.PassageSuffix + "((" + PassageWriter.FrameTypeName + ")__CurrentFrame, " + PassageWriter.TimeName + ", context);"); } else { code.AddLine("var " + PassageWriter.ArcFrameName + " = (" + PassageWriter.FrameTypeName + ")__CurrentFrame;"); code.AddLine("return Run" + PassageWriter.PassageSuffix + "(" + PassageWriter.ArcFrameName + ".Lend(" + PassageWriter.ArcFrameName + "." + PassageWriter.RunFrameField + "), " + PassageWriter.ArcFrameName + ", " + PassageWriter.TimeName + ", context);"); } code.CloseBlock(); code.NewLine(); } private const string FrameBaseWithoutAGeneratedClass = "global::Ariadne.Frame"; // A frame class is only declared by a class this generator wrote, or one it wrote in another assembly. private static bool HasGeneratedFrame(INamedTypeSymbol type) { if (type.DeclaringSyntaxReferences.Length > 0) { return !SaveCodeWriter.HandBuilt(type) && !ScriptClass.WritesOwnResume(type); } return type.GetTypeMembers(PassageWriter.FrameTypeName).Length > 0; } // The frame a derived class's frame extends, so one frame object holds the locals of the whole hierarchy. private static string FrameBase(INamedTypeSymbol type) { for (INamedTypeSymbol baseType = type.BaseType; baseType != null; baseType = baseType.BaseType) { if (HasGeneratedFrame(baseType)) { return baseType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + "." + PassageWriter.FrameTypeName; } } return FrameBaseWithoutAGeneratedClass; } // Tells a lister about every local of the frame, its base frames' first. An arc that calls a passage of this // class lists its frame's locals this way, since it never sees the class itself. private static void WriteListFrameParts(CodeWriter code, INamedTypeSymbol type, ScriptLocals scriptLocals) { code.AddLine("public static void ListParts(global::Ariadne.IArcPartLister lister)"); code.OpenNewBlock(); string frameBase = FrameBase(type); if (frameBase != FrameBaseWithoutAGeneratedClass) { code.AddLine(frameBase + ".ListParts(lister);"); } SaveCodeWriter.WriteLocalListing(code, SaveCodeWriter.SavedLocalsOf(scriptLocals), scriptLocals == null ? new List() : scriptLocals.Sites()); SaveCodeWriter.WriteOwnedRunListing(code, scriptLocals); code.CloseBlock(); } private static void WriteLocalFields(CodeWriter code, PassageLocals locals) { foreach (ISymbol symbol in locals.Hoisted) { code.AddLine("public " + PassageLocals.TypeOf(symbol) .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + " " + locals.FieldName(symbol) + ";"); code.NewLine(); } foreach (SpillTemp temp in locals.Plan.Temps) { string initializer = temp.Initializer == null ? "" : " = " + temp.Initializer; code.AddLine("public " + temp.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + " " + locals.FieldName(temp) + initializer + ";"); code.NewLine(); } } // The class that declares a virtual passage holds whether it is running, for its overrides to share. private static void WriteRunningField(CodeWriter code, IMethodSymbol passage) { if ((passage.IsVirtual || passage.IsAbstract) && !passage.IsOverride) { code.AddLine("public object " + passage.Name + PassageWriter.ActiveSuffix + ";"); code.NewLine(); } } // What the frame of the passage that makes a call keeps while it's paused inside it. private static void WriteSiteFields(CodeWriter code, PassageLocals locals) { foreach (ForeignSite site in locals.Plan.Sites) { if (!site.OnThisArc) { code.AddLine("public " + site.ReceiverType.ToDisplayString( SymbolDisplayFormat.FullyQualifiedFormat) + " " + site.ReceiverField + ";"); code.NewLine(); } code.AddLine("public " + site.FrameFieldType + " " + site.FrameField + ";"); code.NewLine(); if (site.HasOwnedFrame) { code.AddLine("public " + site.OwnedFrameType + " " + site.OwnedFrameField + ";"); code.NewLine(); } } } // A generic passage's locals can be typed by its type parameters, so they live in a frame class with the same // ones, which each call site makes with the type arguments it calls the passage with. The calls it makes are // kept there too, since their types can use those parameters as well. private static void WriteOwnedFrames(CodeWriter code, ScriptClass script, ScriptLocals scriptLocals) { foreach (PassageLocals locals in scriptLocals.ByPassage.Values.Where(each => each.OwnsFrame)) { code.AddLine("[global::System.ComponentModel.EditorBrowsable(" + "global::System.ComponentModel.EditorBrowsableState.Never)]"); code.OpenNewBlock("public partial class " + PassageWriter.OwnedFrameName(locals.Passage) + PassageWriter.TypeParametersOf(locals.Passage) + " : global::Ariadne.Frame" + PassageWriter.ConstraintsOf(script.Methods[locals.Passage])); WriteLocalFields(code, locals); WriteSiteFields(code, locals); code.AddLine("public static void ListParts(global::Ariadne.IArcPartLister lister)"); code.OpenNewBlock(); SaveCodeWriter.WriteLocalListing(code, SaveCodeWriter.SavedLocalsOf(locals), locals.Plan.Sites.ToList()); code.CloseBlock(); code.CloseBlock(); code.NewLine(); } } // Every generated class gets a frame, even one with no locals, so a derived class always finds one to extend. private static void WriteFrame(CodeWriter code, INamedTypeSymbol type, ScriptLocals scriptLocals) { code.AddLine("[global::System.ComponentModel.EditorBrowsable(" + "global::System.ComponentModel.EditorBrowsableState.Never)]"); code.OpenNewBlock("public partial class " + PassageWriter.FrameTypeName + " : " + FrameBase(type)); if (scriptLocals != null) { foreach (PassageLocals locals in scriptLocals.ByPassage.Values) { // A passage that owns a frame keeps its locals and calls there. if (!locals.OwnsFrame) { WriteLocalFields(code, locals); WriteSiteFields(code, locals); WriteRunningField(code, locals.Passage); } } // Made with the arc, so that saving an arc that hasn't run yet finds it. if (scriptLocals.OwnedRun != null) { string runFrame = PassageWriter.OwnedFrameName(scriptLocals.OwnedRun.Passage); code.AddLine("public " + runFrame + " " + PassageWriter.RunFrameField + " = new " + runFrame + "();"); code.NewLine(); } } WriteListFrameParts(code, type, scriptLocals); code.CloseBlock(); code.NewLine(); code.AddLine("[global::System.ComponentModel.EditorBrowsable(" + "global::System.ComponentModel.EditorBrowsableState.Never)]"); code.OpenNewBlock("protected override global::Ariadne.Frame __CreateFrame()"); code.AddLine("return new " + PassageWriter.FrameTypeName + "();"); code.CloseBlock(); code.NewLine(); } private static void WriteSaveLocals(CodeWriter code, ScriptClass script, ScriptLocals scriptLocals) { if (!scriptLocals.Saving.Contains(script.Run)) { return; } code.OpenNewBlock("protected override void __SaveLocals(global::Ariadne.IArcWriter writer)"); if (scriptLocals.OwnedRun == null) { code.AddLine("Run" + PassageWriter.LocalsToSaveSuffix + "((" + PassageWriter.FrameTypeName + ")__CurrentFrame, 0, writer);"); } else { code.AddLine("var " + PassageWriter.ArcFrameName + " = (" + PassageWriter.FrameTypeName + ")__CurrentFrame;"); code.AddLine("Run" + PassageWriter.LocalsToSaveSuffix + "(" + PassageWriter.ArcFrameName + ".Lend(" + PassageWriter.ArcFrameName + "." + PassageWriter.RunFrameField + "), " + PassageWriter.ArcFrameName + ", 0, writer, writer);"); } code.CloseBlock(); code.NewLine(); } private void WriteScript(CodeWriter code, ScriptClass script) { ScriptChecks.Report(script, diagnostics); ScriptLocals scriptLocals = script.Registry.LocalsOf(script, diagnostics); WriteFrame(code, arc.Type, scriptLocals); WriteOwnedFrames(code, script, scriptLocals); // A class with no Run has nothing to resume itself, since only the scripts that derive from it do. if (script.Run != null) { WriteResume(code, script, scriptLocals); WriteSaveLocals(code, script, scriptLocals); } SaveCodeWriter.Write(code, arc.Type, arc.Fields, scriptLocals); foreach (IMethodSymbol passage in script.Passages) { PassageWriter.Write(code, script, scriptLocals, passage); } } /// /// Returns the types is nested in, outermost first. /// public static List ContainersOf(INamedTypeSymbol type) { var returnMe = new List(); for (INamedTypeSymbol container = type.ContainingType; container != null; container = container.ContainingType) { returnMe.Insert(0, container); } return returnMe; } /// /// Returns the header of a part of that only says what it is called. /// public static string PartHeaderOf(INamedTypeSymbol type) { string keyword = "class"; if (type.IsRecord) { keyword = type.TypeKind == TypeKind.Struct ? "record struct" : "record"; } else if (type.TypeKind == TypeKind.Struct) { keyword = "struct"; } else if (type.TypeKind == TypeKind.Interface) { keyword = "interface"; } string returnMe = "partial " + keyword + " " + type.Name; if (type.TypeParameters.Length > 0) { returnMe += "<" + string.Join(", ", type.TypeParameters.Select(parameter => parameter.Name)) + ">"; } return returnMe; } private void WriteClass(CodeWriter code) { bool hasNamespace = !arc.Type.ContainingNamespace.IsGlobalNamespace; if (hasNamespace) { code.NewLine(); code.OpenNewBlock("namespace " + arc.Type.ContainingNamespace.ToDisplayString()); foreach (string directive in Declarations().SelectMany(declaration => UsingsFor(declaration, true)).Distinct()) { code.AddLineAt(column: 0, content: directive); } } else { // The class sits one level in either way, so a script's methods are at the same indentation. code.Indent(); } List containers = ContainersOf(arc.Type); foreach (INamedTypeSymbol container in containers) { code.OpenNewBlock(PartHeaderOf(container)); } code.OpenNewBlock(PartHeaderOf(arc.Type)); if (arc.Script != null) { WriteScript(code, arc.Script); } else { WriteFrame(code, arc.Type, scriptLocals: null); SaveCodeWriter.Write(code, arc.Type, arc.Fields, scriptLocals: null); } code.CloseBlock(); foreach (INamedTypeSymbol container in containers) { code.CloseBlock(); } if (hasNamespace) { code.CloseBlock(); } else { code.Dedent(); } } /// /// Returns the using directives the script file declaring needs repeated in its /// code-behind. /// public static List FileUsings(GeneratedArc arc) { return DeclarationsOf(arc).SelectMany(declaration => UsingsFor(declaration, false)).Distinct().ToList(); } /// /// Returns the generated part of 's class, to sit in its file's code-behind. /// public static string WriteClassOf(GeneratedArc arc, List diagnostics) { var code = new CodeWriter(); new CodeBehindWriter(arc, diagnostics).WriteClass(code); return code.ToString(); } /// /// Returns one file's code-behind: the generated part of every arc class declared in it. /// public static string WriteFile(IReadOnlyList arcs) { var code = new CodeWriter(); code.AddLine("// "); code.AddLine("// Ariadne.Generator's copy of the scripts in this file: the code that actually runs. " + "Edits here are overwritten."); code.AddLine("// In the copy beside the script, #line paths are relative to it; the compiled code " + "uses full paths."); code.AddDirective("#pragma warning disable"); foreach (string directive in arcs.SelectMany(arc => arc.FileUsings.Items).Distinct()) { code.AddLine(directive); } foreach (ArcOutput arc in arcs) { code.NewLine(); code.AddText(arc.ClassText); } return code.ToString(); } public const string CodeBehindSuffix = ".CodeBehind.cs"; /// /// Returns the name to add the code-behind of the script file at under. It /// follows the file's folders below . Ariadne.Generator.targets works out /// the same name to find the file again. /// public static string HintNameOf(string scriptPath, string projectDirectory) { string path = scriptPath.Replace('\\', '/'); string folder = projectDirectory.Replace('\\', '/').TrimEnd('/') + "/"; if (projectDirectory.Length > 0 && path.StartsWith(folder, StringComparison.OrdinalIgnoreCase)) { path = path.Substring(folder.Length); } var segments = new List(); foreach (string segment in path.Split('/')) { if (segment.Length == 0 || segment == ".") { continue; } // A drive's colon and a step up are no use in a name, and would climb out of the output folder. segments.Add(segment == ".." ? "__" : segment.Replace(':', '_')); } segments[segments.Count - 1] = Path.GetFileNameWithoutExtension(segments[segments.Count - 1]); return string.Join("/", segments) + CodeBehindSuffix; } /// /// Returns the code-behind files to write for , and what to report about them. /// public static ArcComposition Compose(EquatableArray outputs, string projectDirectory) { // An order that doesn't depend on which file the compiler happened to hand over first. List sorted = outputs.Items .OrderBy(output => output.TypePlace.FilePath, StringComparer.Ordinal) .ThenBy(output => output.TypePlace.Span.Start) .ToList(); var diagnostics = new List(); foreach (ArcOutput output in sorted) { diagnostics.AddRange(output.ClassProblems.Items); } // Added sources are told apart without regard to case, as they are written out as files. var files = new List(); var pathsByHintName = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (IGrouping file in sorted.Where(output => output.ClassText != null) .GroupBy(output => output.FilePath)) { string hintName; if (string.IsNullOrEmpty(file.Key)) { hintName = file.First().TypeName + CodeBehindSuffix; } else { hintName = HintNameOf(file.Key, projectDirectory); } if (pathsByHintName.ContainsKey(hintName)) { foreach (ArcOutput output in file) { diagnostics.Add(DiagnosticData.Create(ScriptDiagnostics.DuplicateFileName, output.TypePlace, file.Key)); } continue; } pathsByHintName.Add(hintName, file.Key); foreach (ArcOutput output in file) { diagnostics.AddRange(output.ScriptProblems.Items); } files.Add(new ArcFile(hintName, WriteFile(file.ToList()))); } return new ArcComposition(new EquatableArray(files.ToImmutableArray()), new EquatableArray(diagnostics.ToImmutableArray())); } } }