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.
 
 

377 lines
11 KiB

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();
if (e.IsSingleton) sb.AppendLine($"\t\t\t.Add<{e.FullName}>()");
}
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()}>()");
}
}