You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
411 lines
12 KiB
411 lines
12 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.Linq; |
|
using System.Text; |
|
using gaemstone.SourceGen.Structure; |
|
using gaemstone.SourceGen.Utility; |
|
using Microsoft.CodeAnalysis; |
|
using Microsoft.CodeAnalysis.CSharp; |
|
|
|
namespace gaemstone.SourceGen; |
|
|
|
[Generator] |
|
public class ModuleGenerator |
|
: ISourceGenerator |
|
{ |
|
public void Initialize(GeneratorInitializationContext context) |
|
{ |
|
#if DEBUG |
|
// while (!System.Diagnostics.Debugger.IsAttached) |
|
// System.Threading.Thread.Sleep(500); |
|
#endif |
|
context.RegisterForSyntaxNotifications( |
|
() => new RelevantSymbolReceiver()); |
|
} |
|
|
|
private Dictionary<ISymbol, BaseInfo>? _symbolToInfoLookup; |
|
|
|
public BaseInfo? Lookup(ISymbol symbol) |
|
=> (_symbolToInfoLookup ?? throw new InvalidOperationException()) |
|
.TryGetValue(symbol, out var info) ? info : null; |
|
|
|
public void Execute(GeneratorExecutionContext context) |
|
{ |
|
if (context.SyntaxContextReceiver is not RelevantSymbolReceiver receiver) return; |
|
_symbolToInfoLookup = receiver.Symbols; |
|
var infos = receiver.Symbols.Values; |
|
|
|
// Go through all entity infos (types and methods), populate their |
|
// Parent property and add them to their parent's Children property. |
|
foreach (var info in infos.OfType<BaseEntityInfo>()) { |
|
if ((info.Symbol.ContainingType is INamedTypeSymbol parentSymbol) |
|
&& (Lookup(parentSymbol) is TypeEntityInfo parentInfo)) |
|
{ |
|
info.Parent = parentInfo; |
|
parentInfo.Children.Add(info); |
|
} |
|
} |
|
|
|
// Go through all the method infos and lookup / construct |
|
// their parameter infos from the method's parameters. |
|
foreach (var info in infos.OfType<MethodEntityInfo>()) { |
|
foreach (var paramSymbol in info.Symbol.Parameters) { |
|
if (Lookup(paramSymbol) is not ParameterInfo paramInfo) |
|
paramInfo = new ParameterInfo(paramSymbol); |
|
info.Parameters.Add(paramInfo); |
|
paramInfo.Parent = info; |
|
} |
|
} |
|
|
|
// Validate all instances of base info without a Parent / Method set. |
|
// Nested infos are validated by their containing info. |
|
foreach (var info in infos.Where(info => info.Parent == null)) |
|
info.Validate(); |
|
|
|
// Report all the diagnostics we found. |
|
foreach (var info in infos) |
|
foreach (var diag in info.Diagnostics) |
|
context.ReportDiagnostic(diag); |
|
|
|
foreach (var module in infos.OfType<ModuleEntityInfo>()) { |
|
if (module.IsErrored) continue; |
|
var sb = new StringBuilder(); |
|
AppendHeader(sb, module.Namespace); |
|
AppendModuleType(sb, module); |
|
context.AddSource($"{module.FullName}.g.cs", sb.ToString()); |
|
} |
|
|
|
_symbolToInfoLookup = null; |
|
} |
|
|
|
private void AppendHeader(StringBuilder sb, string @namespace) |
|
=> sb.AppendLine($$""" |
|
// <auto-generated/> |
|
using System.Collections.Generic; |
|
using System.Collections.Immutable; |
|
using gaemstone.ECS; |
|
using gaemstone.ECS.Utility; |
|
|
|
namespace {{ @namespace }}; |
|
|
|
"""); |
|
|
|
private void AppendModuleType(StringBuilder sb, ModuleEntityInfo module) |
|
{ |
|
var type = module.Symbol.IsValueType ? "struct" : "class"; |
|
sb.AppendLine($$""" |
|
public partial {{ type }} {{ module.Name }} |
|
: IModule |
|
"""); |
|
|
|
var modulePath = module.GetModulePath().ToStringLiteral(); |
|
sb.AppendLine($$""" |
|
{ |
|
static string IModule.Path { get; } = {{ modulePath }}; |
|
|
|
static bool IModule.IsBuiltIn { get; } = {{( module.IsBuiltIn ? "true" : "false" )}}; |
|
|
|
"""); |
|
|
|
// TODO: Built-in modules should not have dependencies. |
|
var dependencies = module.GetDependencies().ToList(); |
|
sb.Append("\tstatic IReadOnlyList<string> IModule.Dependencies { get; } = "); |
|
if (dependencies.Count > 0) { |
|
sb.AppendLine("ImmutableList.Create("); |
|
foreach (var dependency in dependencies) |
|
sb.AppendLine($"\t\t{dependency.ToStringLiteral()},"); |
|
sb.Length -= 2; sb.AppendLine(); |
|
sb.AppendLine("\t);"); |
|
} else sb.AppendLine("ImmutableList.Create<string>();"); |
|
sb.AppendLine(); |
|
|
|
sb.AppendLine($$""" |
|
static void IModule.OnEnable<T>(Entity<T> module) |
|
{ |
|
var world = module.World; |
|
"""); |
|
|
|
// TODO: Might want to add things related to the module entity. |
|
|
|
AppendEntityRegistration(sb, module); |
|
AppendMethodRegistration(sb, module); |
|
AppendEntityToAdd(sb, module); |
|
AppendShimMethods(sb, module); |
|
|
|
// TODO: Can BuiltIn modules have systems and such? |
|
|
|
if (module.HasLifetimeInterface) |
|
sb.AppendLine("\t\tOnEnable(module);"); |
|
|
|
sb.AppendLine($$""" |
|
} |
|
|
|
static void IModule.OnDisable<T>(Entity<T> module) |
|
{ |
|
"""); |
|
|
|
if (module.IsBuiltIn) |
|
sb.AppendLine("\t\tthrow new global::System.InvalidOperationException();"); |
|
if (module.HasLifetimeInterface) |
|
sb.AppendLine("\t\tOnDisable(module);"); |
|
|
|
sb.AppendLine($$""" |
|
} |
|
} |
|
"""); |
|
} |
|
|
|
private void AppendEntityRegistration( |
|
StringBuilder sb, ModuleEntityInfo module) |
|
{ |
|
var entities = module.Children |
|
.OfType<TypeEntityInfo>() |
|
.Where(e => !e.IsErrored) |
|
.ToList(); |
|
if (entities.Count == 0) return; |
|
|
|
sb.AppendLine(); |
|
sb.AppendLine("\t\t// Register entities."); |
|
foreach (var e in entities) { |
|
var @var = $"_{e.Name}_Entity"; |
|
var path = (e.EntityPath ?? e.Name).ToStringLiteral(); |
|
|
|
// When looking for a BuiltIn entity, error if it doesn't exist. |
|
var lookupFunc = (e.IsBuiltIn ? "LookupPathOrThrow" : "New"); |
|
sb.AppendLine($"\t\tvar {@var} = world.{lookupFunc}({path}, module)"); |
|
|
|
// Since this is a custom gaemstone tag, we want to add it even for [BuiltIn] modules. |
|
if (e.IsRelation) sb.AppendLine("\t\t\t.Add<gaemstone.Doc.Relation>()"); |
|
|
|
// Other than [Relation], we don't add anything to entities defined in [BuiltIn] modules. |
|
if (module.IsBuiltIn) |
|
{ |
|
sb.AppendLine($"\t\t\t.CreateLookup<{e.FullName}>()"); |
|
} |
|
else |
|
{ |
|
if (e.EntitySymbol != null) |
|
sb.AppendLine($"\t\t\t.Symbol({e.EntitySymbol.ToStringLiteral()})"); |
|
|
|
// Tags and relations in Flecs are marked as empty components. |
|
if (e.IsTag || e.IsRelation) sb.AppendLine("\t\t\t.Add<gaemstone.Flecs.Core.Component>()"); |
|
if (e.IsTag && e.IsRelation) sb.AppendLine("\t\t\t.Add<gaemstone.Flecs.Core.Tag>()"); |
|
|
|
sb.Append( "\t\t\t"); |
|
if (!e.IsBuiltIn) sb.Append(".Build()"); |
|
if (e.IsComponent) sb.Append($".InitComponent<{e.FullName}>()"); |
|
else sb.Append($".CreateLookup<{e.FullName}>()"); |
|
sb.AppendLine(); |
|
|
|
// I don't think it makes sense to have singletons pre-initialized to zero. |
|
// Especially for singletons that are reference types, which would default to null. |
|
// if (e.IsSingleton) sb.AppendLine($"\t\t\t.Add<{e.FullName}>()"); |
|
// TODO: Look into if it would be possible to detect if we have field initializers. |
|
} |
|
|
|
sb.Insert(sb.Length - 1, ";"); |
|
} |
|
} |
|
|
|
private void AppendMethodRegistration( |
|
StringBuilder sb, ModuleEntityInfo module) |
|
{ |
|
var methods = module.Children |
|
.OfType<MethodEntityInfo>() |
|
.Where(e => !e.IsErrored) |
|
.ToList(); |
|
if (methods.Count == 0) return; |
|
|
|
sb.AppendLine(); |
|
sb.AppendLine("\t\t// Register systems / observers."); |
|
foreach (var m in methods) { |
|
var @var = $"_{m.Name}_Entity"; |
|
var path = (m.EntityPath ?? m.Name).ToStringLiteral(); |
|
sb.AppendLine($"\t\tvar {@var} = world.New({path}, module)"); |
|
|
|
sb.Append("\t\t\t.Build()"); |
|
if (m.IsSystem) sb.AppendLine(".InitSystem("); |
|
if (m.IsObserver) sb.AppendLine(".InitObserver("); |
|
|
|
if (m.IsIteratorOnly) { |
|
var expression = m.Expression.ToStringLiteral(); |
|
sb.AppendLine($"\t\t\t\tnew({expression}), {m.Name},"); |
|
} else { |
|
sb.AppendLine("\t\t\t\tnew("); |
|
foreach (var p in m.Parameters) |
|
if (p.HasTerm) { |
|
for (var i = 0; i < p.TermTypes.Count; i++) { |
|
var term = p.TermTypes[i]; |
|
var isLastTerm = (i == p.TermTypes.Count - 1); |
|
|
|
sb.Append($"\t\t\t\t\tnew Term("); |
|
switch (term) { |
|
case ITypeSymbol type: |
|
AppendTypeEntity(sb, module, type); |
|
break; |
|
case ParameterInfo.Pair pair: |
|
AppendTypeEntity(sb, module, pair.Relation); |
|
sb.Append(','); |
|
AppendTypeEntity(sb, module, pair.Target); |
|
break; |
|
default: throw new InvalidOperationException( |
|
$"Unexpected term type {term.GetType()}"); |
|
} |
|
sb.Append(')'); |
|
|
|
if (p.Source != null) { |
|
sb.Append("{ Source = "); |
|
AppendTypeEntity(sb, module, p.Source); |
|
sb.Append(" }"); |
|
} |
|
|
|
// The last term in a group of OR terms must not have the TermOperKind.Or set. |
|
if (p.IsOr && !isLastTerm) sb.Append(".Or"); |
|
|
|
sb.Append(p.Kind switch { |
|
ParameterKind.Has => ".None", |
|
ParameterKind.Not => ".Not", |
|
ParameterKind.Ref => ".InOut", |
|
ParameterKind.Out => ".Out", |
|
_ when !p.IsValueType => ".InOut", // Reference types always imply writability. |
|
_ => ".In", |
|
}); |
|
|
|
if (p.IsNullable) sb.Append(".Optional"); |
|
|
|
sb.AppendLine(","); |
|
} |
|
} |
|
if (m.Parameters.Any(p => p.HasTerm)) |
|
{ sb.Length -= 2; sb.AppendLine(); } |
|
sb.AppendLine( "\t\t\t\t),"); |
|
sb.AppendLine($"\t\t\t\t_{m.Name}_Shim,"); |
|
} |
|
|
|
if (m.IsObserver) |
|
foreach (var ev in m.ObserverEvents!) { |
|
sb.Append("\t\t\t\t"); |
|
AppendTypeEntity(sb, module, ev); |
|
sb.AppendLine(","); |
|
} |
|
|
|
sb.Length -= 2; |
|
sb.AppendLine(");"); |
|
} |
|
} |
|
|
|
private void AppendEntityToAdd( |
|
StringBuilder sb, ModuleEntityInfo module) |
|
{ |
|
var entities = module.Children |
|
.Where(e => !e.IsErrored) |
|
.Where(e => e.HasEntitiesToAdd) |
|
.ToList(); |
|
if (entities.Count == 0) return; |
|
|
|
sb.AppendLine(); |
|
sb.AppendLine("\t\t// Add things to entities."); |
|
foreach (var e in entities) { |
|
var @var = $"_{e.Name}_Entity"; |
|
foreach (var a in e.EntitiesToAdd) |
|
sb.AppendLine($"\t\t{@var}.Add<{a.GetFullName()}>();"); |
|
foreach (var (r, t) in e.RelationsToAdd) |
|
sb.AppendLine($"\t\t{@var}.Add<{r.GetFullName()}, {t.GetFullName()}>();"); |
|
foreach (var (c, args) in e.ComponentsToAdd) |
|
sb.AppendLine($"\t\t{@var}.Set(new {c.GetFullName()}({string.Join(", ", args.Select(a => a.ToCSharpString()))}));"); |
|
|
|
// If system doesn't have an explicit phase set, default to OnUpdate. |
|
if (e is MethodEntityInfo { IsSystem: true, HasPhaseSet: false }) |
|
sb.AppendLine($"\t\t{@var}.Add<gaemstone.Flecs.Core.DependsOn, gaemstone.Flecs.Pipeline.OnUpdate>();"); |
|
} |
|
} |
|
|
|
private void AppendShimMethods( |
|
StringBuilder sb, ModuleEntityInfo module) |
|
{ |
|
var methods = module.Children |
|
.OfType<MethodEntityInfo>() |
|
.Where(m => !m.IsErrored) |
|
.Where(m => !m.IsIteratorOnly) |
|
.ToList(); |
|
|
|
foreach (var method in methods) |
|
AppendShimMethod(sb, module, method); |
|
} |
|
|
|
private void AppendShimMethod( |
|
StringBuilder sb, ModuleEntityInfo module, |
|
MethodEntityInfo method) |
|
{ |
|
sb.AppendLine(); |
|
sb.AppendLine($$""" |
|
void _{{ method.Name }}_Shim(Iterator<T> iter) |
|
{ |
|
"""); |
|
|
|
foreach (var param in method.Parameters) |
|
if (param.HasField) |
|
sb.Append($"\t\t\tvar _{param.Name}_Field = ") |
|
.Append(param.IsNullable ? "iter.FieldOrEmpty" : "iter.Field") |
|
.AppendLine($"<{param.FieldType!.GetFullName()}>({param.TermIndex});"); |
|
|
|
// When the shim method is called, there's guaranteed to be at least one match. |
|
// iter.Count may be 0 when there's no variable entities that can be matched. |
|
// For example for a query that only matches singleton entities. |
|
sb.AppendLine("\t\t\tfor (var i = 0; i == 0 || i < iter.Count; i++) {"); |
|
|
|
sb.Append($"\t\t\t\t{method.Name}"); |
|
if (method.IsGeneric) sb.Append("<T>"); |
|
sb.Append($"("); |
|
foreach (var param in method.Parameters) { |
|
// TODO: Support [Or<...>] |
|
if (param.IsOr && (param.Kind != ParameterKind.Has)) |
|
throw new NotSupportedException($"Or<...> parameter not yet supported"); |
|
switch (param.Kind) { |
|
case ParameterKind.Unique: |
|
sb.Append(param.UniqueReplacement); |
|
break; |
|
|
|
case ParameterKind.In: |
|
case ParameterKind.Out: |
|
case ParameterKind.Ref: |
|
var modifier = param.Kind.ToString().ToLowerInvariant(); |
|
sb.Append(modifier).Append(' '); |
|
goto case ParameterKind.Normal; |
|
|
|
case ParameterKind.Normal: |
|
case ParameterKind.Nullable: |
|
// FIXME: Handle pairs. |
|
sb.Append($"_{param.Name}_Field") |
|
.Append(param.IsNullable ? ".GetOrNull(i)" : "[i]"); |
|
break; |
|
|
|
case ParameterKind.Has: |
|
case ParameterKind.Not: |
|
sb.Append("default"); |
|
break; |
|
} |
|
sb.Append(", "); |
|
} |
|
if (method.Parameters.Any()) |
|
sb.Length -= 2; |
|
sb.AppendLine(");"); |
|
|
|
sb.AppendLine($$""" |
|
} |
|
} |
|
"""); |
|
} |
|
|
|
|
|
private void AppendTypeEntity( |
|
StringBuilder sb, ModuleEntityInfo module, |
|
ITypeSymbol type) |
|
{ |
|
// TODO: Cache entity lookup. |
|
var found = module.Children.Where(c => !c.IsErrored) |
|
.Any(c => SymbolEqualityComparer.Default.Equals(c.Symbol, type)); |
|
sb.Append(found ? $"_{type.Name}_Entity" |
|
: $"world.Entity<{type.GetFullName()}>()"); |
|
} |
|
}
|
|
|