using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
namespace Ariadne.Generator
{
///
/// Writes the methods that hand an arc's saved fields and locals to an IArcWriter, and the constructors that take
/// them back from an IArcReader.
///
public static class SaveCodeWriter
{
public const string ReaderName = "Ariadne.IArcReader";
public const string ListPartsName = "__ListParts";
// The generator can't reference the runtime type, so a saved local is found by name.
private const string TriggerWaitName = "Ariadne.TriggerWait";
///
/// Returns true if writes its own __SaveFields, and so its loading too.
///
public static bool HandBuilt(INamedTypeSymbol type)
{
return type.GetMembers("__SaveFields").Any();
}
private static string TypeName(ITypeSymbol type)
{
return type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}
private static string Quoted(string key)
{
return "\"" + key + "\"";
}
// Each writer call maps to what it saves, so a save library's analyzer that rejects the type reports it on the
// script's own line. Reader calls stay hidden, so each problem is reported once.
private static void WriteSaveFields(CodeWriter code, List fields)
{
code.OpenNewBlock("protected override void __SaveFields(global::Ariadne.IArcWriter writer)");
code.AddLine("base.__SaveFields(writer);");
foreach (SavedMember field in fields)
{
PassageWriter.AddMapped(code, field.Declared.Locations.First(), "writer.Field<" +
TypeName(field.Type) + ">(" + Quoted(field.Key) + ", " + field.Access + ");");
}
code.AddDirective("#line default");
code.CloseBlock();
code.NewLine();
}
///
/// Returns the constructor writes itself to load from a save, or null if it has none.
///
public static IMethodSymbol OwnLoader(INamedTypeSymbol type)
{
return type.InstanceConstructors.FirstOrDefault(constructor => !constructor.IsImplicitlyDeclared
&& constructor.Parameters.Length == 1
&& constructor.Parameters[0].Type.ToDisplayString() == ReaderName);
}
///
/// Returns how visible 's loading constructor must be.
///
public static Accessibility LoaderAccessibility(INamedTypeSymbol type)
{
// A class others derive from has its loader called by them.
if (type.IsSealed)
{
return Accessibility.Private;
}
return Accessibility.Protected;
}
///
/// Holds what is needed to save one local, whether it came from the script, from a spilled expression, or is
/// the receiver of a call to another arc.
///
public class SavedLocal
{
///
/// Holds what the frame tells the local apart by.
///
public readonly string Key;
///
/// Holds what the local is written and read as, within the table of the call it belongs to.
///
public readonly string Name;
public readonly string FieldName;
public readonly ITypeSymbol Type;
public readonly Location Where;
///
/// Holds the call the local is the receiver of, or null for any other local.
///
public readonly ForeignSite ReceiverOf;
public SavedLocal(string key, string fieldName, ITypeSymbol type, Location where)
{
Key = key;
Name = key;
FieldName = fieldName;
Type = type;
Where = where;
}
public SavedLocal(ForeignSite site)
{
Key = site.ReceiverKey;
Name = ForeignSite.ReceiverSuffix;
FieldName = site.ReceiverField;
Type = site.ReceiverType;
Where = site.Where;
ReceiverOf = site;
}
}
private static void WriteLoaders(CodeWriter code, INamedTypeSymbol type, List fields)
{
string access = "protected";
if (LoaderAccessibility(type) == Accessibility.Private)
{
access = "private";
}
// Declaring any constructor takes away the one C# writes for a class that has none.
if (type.InstanceConstructors.All(constructor => constructor.IsImplicitlyDeclared))
{
string implicitAccess = "public";
if (type.IsAbstract)
{
implicitAccess = "protected";
}
code.OpenNewBlock(implicitAccess + " " + type.Name + "()");
code.CloseBlock();
code.NewLine();
}
code.OpenNewBlock(access + " " + type.Name + "(global::Ariadne.IArcReader reader, " +
"global::Ariadne.AutoLoadEveryField load) : base(reader)");
foreach (SavedMember field in fields)
{
string fieldType = TypeName(field.Type);
string line = "{ if (reader.TryField<" + fieldType + ">(" + Quoted(field.Key) + ", out " + fieldType +
" loaded)) { " + field.Access + " = loaded; }";
// A load doesn't run the constructors, so a member with no initializer would come back as its type's
// default. That's a null the script's first use of it trips over, or a quiet zero.
if (!field.HasInitializer)
{
line += " else if (!reader.HasField(" + Quoted(field.Key) + ")) { throw new " +
"global::System.IO.InvalidDataException(" + Quoted(type.Name + "'s save has no field '" +
field.Key + "', which has no initializer to fall back on. Give it one.") + "); }";
}
code.AddLine(line + " }");
}
code.CloseBlock();
code.NewLine();
if (OwnLoader(type) != null)
{
return;
}
code.AddLine(access + " " + type.Name + "(global::Ariadne.IArcReader reader)");
code.Indent();
code.AddLine(": this(reader, default(global::Ariadne.AutoLoadEveryField))");
code.Dedent();
code.OpenNewBlock();
code.CloseBlock();
code.NewLine();
}
///
/// Writes the lines that tell lister about each of , and about each local of
/// the frame a call in runs its passage with, in a table of its own.
///
public static void WriteLocalListing(CodeWriter code, List locals, List sites)
{
foreach (SavedLocal local in locals.Where(each => each.ReceiverOf == null))
{
code.AddLine("lister.Local<" + TypeName(local.Type) + ">(" + Quoted(local.Name) + ");");
}
foreach (ForeignSite site in sites)
{
if (site.Polymorphic)
{
code.AddLine("lister.Polymorphic(" + Quoted(site.ScopeName) + ", typeof(" + site.FrameDefinition +
"));");
continue;
}
// The lister answers null for a call whose frames it is already listing further up, which it takes as
// the same again. Listing it would never end.
string frames = "typeof(" + site.FrameType + ")";
if (site.HasOwnedFrame)
{
frames += ", typeof(" + site.OwnedFrameType + ")";
}
code.OpenNewBlock();
code.AddLine("global::Ariadne.IArcPartLister scope = lister.Scope(" + Quoted(site.ScopeName) + ", " +
frames + ");");
code.OpenNewBlock("if (scope != null)");
if (!site.OnThisArc)
{
code.AddLine("scope.Local<" + TypeName(site.ReceiverType) + ">(" +
Quoted(ForeignSite.ReceiverSuffix) + ");");
}
code.AddLine(site.FrameType + ".ListParts(scope);");
if (site.HasOwnedFrame)
{
code.AddLine(site.OwnedFrameType + ".ListParts(scope);");
}
code.CloseBlock();
code.CloseBlock();
}
}
///
/// Writes the line that tells lister about the locals of Run, if it keeps them in a frame of its own.
/// They sit beside the arc's own, since that frame writes to the arc's level.
///
public static void WriteOwnedRunListing(CodeWriter code, ScriptLocals scriptLocals)
{
if (scriptLocals != null && scriptLocals.OwnedRun != null)
{
code.AddLine(PassageWriter.OwnedFrameName(scriptLocals.OwnedRun.Passage) + ".ListParts(lister);");
}
}
// Private, so a derived class's own copy hides nothing. Loading calls each class's copy on its own.
private static void WriteListParts(CodeWriter code, List fields, List locals,
List sites, ScriptLocals scriptLocals)
{
code.AddLine("[global::System.ComponentModel.EditorBrowsable(" +
"global::System.ComponentModel.EditorBrowsableState.Never)]");
code.OpenNewBlock("private static void " + ListPartsName + "(global::Ariadne.IArcPartLister lister)");
foreach (SavedMember field in fields)
{
code.AddLine("lister.Field<" + TypeName(field.Type) + ">(" + Quoted(field.Key) + ");");
}
WriteLocalListing(code, locals, sites);
WriteOwnedRunListing(code, scriptLocals);
code.CloseBlock();
code.NewLine();
}
private static void WriteSaveLocal(CodeWriter code, List locals)
{
code.OpenNewBlock("public override void SaveLocal(string key, global::Ariadne.IArcWriter writer)");
// A frame with no local of its own would make C# warn of an empty switch.
if (locals.Count > 0)
{
code.OpenNewBlock("switch (key)");
foreach (SavedLocal local in locals)
{
code.AddLine("case " + Quoted(local.Key) + ":");
code.Indent();
PassageWriter.AddMapped(code, local.Where, "writer.Local<" + TypeName(local.Type) + ">(" +
Quoted(local.Name) + ", this." + local.FieldName + ");");
code.AddDirective("#line hidden");
code.AddLine("return;");
code.Dedent();
}
code.CloseBlock();
code.AddDirective("#line default");
code.NewLine();
}
code.AddLine("base.SaveLocal(key, writer);");
code.CloseBlock();
code.NewLine();
}
private static void WriteLoadLocals(CodeWriter code, List locals, List sites,
PassageLocals ownedRun)
{
code.OpenNewBlock("public override void LoadLocals(global::Ariadne.IArcReader reader)");
// A save can hold calls nested as deep as a cycle of calls ran, and loading follows them down.
if (sites.Count > 0 || ownedRun != null)
{
code.AddLine(
"global::System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack();");
}
code.AddLine("base.LoadLocals(reader);");
if (ownedRun != null)
{
code.AddLine("this." + PassageWriter.RunFrameField + ".LoadLocals(reader);");
}
foreach (SavedLocal local in locals.Where(each => each.ReceiverOf == null))
{
string localType = TypeName(local.Type);
code.AddLine("{ if (reader.TryLocal<" + localType + ">(" + Quoted(local.Name) + ", out " + localType +
" loaded)) { this." + local.FieldName + " = loaded; } }");
}
// A receiver in the save, or a table for a call on this arc, means the arc was paused inside that call.
// Any other call makes its frame when it begins, since making one here for a recursive call never ends.
foreach (ForeignSite site in sites)
{
// Each site's block keeps its scope apart from the next site's.
code.OpenNewBlock();
if (site.OnThisArc)
{
code.OpenNewBlock("if (reader.TryScope(" + Quoted(site.ScopeName) +
", out global::Ariadne.IArcReader scope))");
if (site.Polymorphic)
{
code.AddLine("this." + site.FrameField + " = global::Ariadne.Frame.Build(typeof(" +
site.FrameDefinition + "), scope);");
}
else
{
code.AddLine("this." + site.FrameField + " = new " + site.FrameType + "();");
}
code.AddLine("this." + site.FrameField + ".LoadLocals(scope);");
code.CloseBlock();
code.CloseBlock();
code.NewLine();
continue;
}
string receiverType = TypeName(site.ReceiverType);
code.OpenNewBlock("if (reader.TryScope(" + Quoted(site.ScopeName) +
", out global::Ariadne.IArcReader scope))");
code.AddLine("{ if (scope.TryLocal<" + receiverType + ">(" + Quoted(ForeignSite.ReceiverSuffix) +
", out " + receiverType + " loaded)) { this." + site.ReceiverField + " = loaded; } }");
code.OpenNewBlock("if (this." + site.ReceiverField + " != null)");
code.AddLine("this." + site.FrameField + " = (" + site.FrameType + ")this." + site.ReceiverField +
".NewFrame();");
code.AddLine("this." + site.FrameField + ".LoadLocals(scope);");
if (site.HasOwnedFrame)
{
code.AddLine("this." + site.OwnedFrameField + " = new " + site.OwnedFrameType + "();");
code.AddLine("this." + site.OwnedFrameField + ".LoadLocals(scope);");
}
code.CloseBlock();
code.CloseBlock();
code.CloseBlock();
code.NewLine();
}
code.CloseBlock();
code.NewLine();
}
// Writes a method that does something to every trigger wait of the frame, and then of the frames of the calls
// the arc is paused in, since their waits are part of the same arc.
private static void WriteWaitWalk(CodeWriter code, string name, string parameters, string arguments,
string onWait, List waits, List sites, PassageLocals ownedRun)
{
code.OpenNewBlock("public override void " + name + "(" + parameters + ")");
if (sites.Count > 0 || ownedRun != null)
{
code.AddLine(
"global::System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack();");
}
code.AddLine("base." + name + "(" + arguments + ");");
if (ownedRun != null)
{
code.AddLine("this." + PassageWriter.RunFrameField + "." + name + "(" + arguments + ");");
}
foreach (SavedLocal wait in waits)
{
code.AddLine("this." + wait.FieldName + "." + onWait + ";");
}
foreach (ForeignSite site in sites)
{
code.AddLine("if (this." + site.FrameField + " != null) { this." + site.FrameField + "." + name +
"(" + arguments + "); }");
if (site.HasOwnedFrame)
{
code.AddLine("if (this." + site.OwnedFrameField + " != null) { this." + site.OwnedFrameField +
"." + name + "(" + arguments + "); }");
}
}
code.CloseBlock();
code.NewLine();
}
// A trigger wait is told which scheduler holds its arc, whether it was loaded or added again. It is ended when
// the arc restarts, so that nothing it waited on wakes the arc from a wait the restart has abandoned.
private static void WriteWaitHooks(CodeWriter code, List locals, List sites,
PassageLocals ownedRun)
{
List waits = locals.Where(each => each.ReceiverOf == null
&& each.Type.ToDisplayString() == TriggerWaitName).ToList();
if (waits.Count == 0 && sites.Count == 0 && ownedRun == null)
{
return;
}
WriteWaitWalk(code, "AttachWaits", "global::Ariadne.WakeSlot slot", "slot", "Attach(slot)", waits, sites,
ownedRun);
WriteWaitWalk(code, "EndWaits", "", "", "Expire()", waits, sites, ownedRun);
}
// The frame class's fields are with the passage copies. Its writer calls come first, where a save library's
// analyzer sees them, and the rest follow with warnings off again so that the reader calls stay unchecked.
private static void WriteFrameSaving(CodeWriter code, string frameClass, List locals,
List sites, PassageLocals ownedRun = null)
{
if (locals.Count > 0)
{
code.OpenNewBlock("public partial class " + frameClass);
WriteSaveLocal(code, locals);
code.CloseBlock();
code.NewLine();
}
code.AddDirective("#pragma warning disable");
code.OpenNewBlock("public partial class " + frameClass);
WriteLoadLocals(code, locals, sites, ownedRun);
WriteWaitHooks(code, locals, sites, ownedRun);
code.CloseBlock();
code.NewLine();
}
///
/// Returns every local saves, in key order, with the receiver of each call to
/// another arc. It's empty for an arc that isn't a script, which is passed as null.
///
public static List SavedLocalsOf(ScriptLocals scriptLocals)
{
var returnMe = new List();
if (scriptLocals == null)
{
return returnMe;
}
foreach (PassageLocals passage in scriptLocals.ByPassage.Values)
{
// A passage that owns a frame has it save its locals.
if (!passage.OwnsFrame)
{
returnMe.AddRange(SavedLocalsOf(passage));
}
}
return returnMe.OrderBy(local => local.Key, System.StringComparer.Ordinal).ToList();
}
///
/// Returns the locals and temps of that are saved, and the receiver of each call it
/// makes to another arc, in key order.
///
public static List SavedLocalsOf(PassageLocals passage)
{
var returnMe = new List();
foreach (ISymbol symbol in passage.Saved)
{
returnMe.Add(new SavedLocal(passage.Key(symbol), passage.FieldName(symbol),
PassageLocals.TypeOf(symbol), symbol.Locations.First()));
}
foreach (SpillTemp temp in passage.Plan.Temps.Where(temp => temp.Saved))
{
returnMe.Add(new SavedLocal(passage.Key(temp), passage.FieldName(temp), temp.Type, temp.Where));
}
foreach (ForeignSite site in passage.Plan.Sites.Where(each => !each.OnThisArc))
{
returnMe.Add(new SavedLocal(site));
}
return returnMe.OrderBy(local => local.Key, System.StringComparer.Ordinal).ToList();
}
///
/// Writes the constructors that load one class from a save, and the methods that save it.
///
/// The script's saved locals, or null for an arc that isn't a script.
public static void Write(CodeWriter code, INamedTypeSymbol type, List fields,
ScriptLocals scriptLocals)
{
List locals = SavedLocalsOf(scriptLocals);
List sites = scriptLocals == null ? new List() : scriptLocals.Sites();
// A class that writes its own __SaveFields is hand-built, so it loads itself too.
if (!HandBuilt(type))
{
WriteLoaders(code, type, fields);
WriteListParts(code, fields, locals, sites, scriptLocals);
}
PassageLocals ownedRun = scriptLocals == null ? null : scriptLocals.OwnedRun;
List ownedFrames = new List();
if (scriptLocals != null)
{
ownedFrames = scriptLocals.ByPassage.Values
.Where(passage => passage.OwnsFrame
&& (SavedLocalsOf(passage).Count > 0 || passage.Plan.Sites.Count > 0)).ToList();
}
if (fields.Count == 0 && locals.Count == 0 && sites.Count == 0 && ownedFrames.Count == 0)
{
return;
}
// The rest of the file is the script's own code, already checked where it's written. These methods are
// new, so a save library's analyzer has to see them.
code.AddDirective("#pragma warning restore");
if (fields.Count > 0)
{
WriteSaveFields(code, fields);
}
if (locals.Count > 0 || sites.Count > 0 || ownedRun != null)
{
WriteFrameSaving(code, PassageWriter.FrameTypeName, locals, sites, ownedRun);
}
// Each frame's writer calls follow a restore, since the one before it ends with warnings off again.
foreach (PassageLocals passage in ownedFrames)
{
code.AddDirective("#pragma warning restore");
WriteFrameSaving(code, PassageWriter.OwnedFrameName(passage.Passage) +
PassageWriter.TypeParametersOf(passage.Passage), SavedLocalsOf(passage),
passage.Plan.Sites.ToList());
}
code.AddDirective("#pragma warning disable");
}
}
}