|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Text;
|
|
|
|
using gaemstone.SourceGen.Structure;
|
|
|
|
using gaemstone.SourceGen.Utility;
|
|
|
|
using Microsoft.CodeAnalysis;
|
|
|
|
|
|
|
|
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" )}};
|
|
|
|
|
|
|
|
""");
|
|
|
|
|
|
|
|
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.Initialize<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?
|
|
|
|
|
|
|
|
sb.AppendLine("\t}");
|
|
|
|
|
|
|
|
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();
|
|
|
|
sb.AppendLine($"\t\tvar {@var} = world.New({path}, module)");
|
|
|
|
if (e.EntitySymbol != null)
|
|
|
|
sb.AppendLine($"\t\t\t.Symbol({e.EntitySymbol.ToStringLiteral()})");
|
|
|
|
|
|
|
|
// 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>()");
|
|
|
|
|
|
|
|
if (module.IsBuiltIn)
|
|
|
|
{
|
|
|
|
sb.AppendLine($"\t\t\t.Build().CreateLookup<{e.FullName}>()");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// TODO: if (e.IsPublic) sb.AppendLine($"\t\t\t.Symbol(\"{e.Name}\")");
|
|
|
|
|
|
|
|
// 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.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 => ".None.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()}>();");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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});");
|
|
|
|
|
|
|
|
sb.AppendLine("\t\t\tfor (var 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()}>()");
|
|
|
|
}
|
|
|
|
}
|