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.
 
 

425 lines
16 KiB

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using gaemstone.ECS;
using GCHandle = System.Runtime.InteropServices.GCHandle;
namespace gaemstone.Utility.IL;
// TODO: Support tuple syntax to match relationship pairs.
public unsafe class IterActionGenerator
{
private static readonly ConstructorInfo _entityRefCtor = typeof(EntityRef).GetConstructors().Single();
private static readonly PropertyInfo _iteratorWorldProp = typeof(Iterator).GetProperty(nameof(Iterator.World))!;
private static readonly PropertyInfo _iteratorDeltaTimeProp = typeof(Iterator).GetProperty(nameof(Iterator.DeltaTime))!;
private static readonly PropertyInfo _iteratorCountProp = typeof(Iterator).GetProperty(nameof(Iterator.Count))!;
private static readonly MethodInfo _iteratorEntityMethod = typeof(Iterator).GetMethod(nameof(Iterator.Entity))!;
private static readonly MethodInfo _iteratorFieldMethod = typeof(Iterator).GetMethod(nameof(Iterator.Field))!;
private static readonly MethodInfo _iteratorFieldOrEmptyMethod = typeof(Iterator).GetMethod(nameof(Iterator.FieldOrEmpty))!;
private static readonly MethodInfo _handleFromIntPtrMethod = typeof(GCHandle).GetMethod(nameof(GCHandle.FromIntPtr))!;
private static readonly PropertyInfo _handleTargetProp = typeof(GCHandle).GetProperty(nameof(GCHandle.Target))!;
private static readonly ConditionalWeakTable<MethodInfo, IterActionGenerator> _cache = new();
private static readonly Dictionary<Type, Action<ILGeneratorWrapper, IArgument<Iterator>>> _globalUniqueParameters = new() {
[typeof(World)] = (IL, iter) => { IL.Load(iter, _iteratorWorldProp); },
[typeof(Universe)] = (IL, iter) => { IL.Load(iter, _iteratorWorldProp); IL.Cast(typeof(Universe)); },
[typeof(TimeSpan)] = (IL, iter) => { IL.Load(iter, _iteratorDeltaTimeProp); },
};
private static readonly Dictionary<Type, Action<ILGeneratorWrapper, IArgument<Iterator>, ILocal<int>>> _uniqueParameters = new() {
[typeof(Iterator)] = (IL, iter, i) => { IL.Load(iter); },
[typeof(EntityRef)] = (IL, iter, i) => { IL.Load(iter); IL.Load(i); IL.Call(_iteratorEntityMethod); },
};
public World World { get; }
public MethodInfo Method { get; }
public IReadOnlyList<ParamInfo> Parameters { get; }
public IReadOnlyList<Term> Terms { get; }
public Action<object?, Iterator> GeneratedAction { get; }
public string ReadableString { get; }
public void RunWithTryCatch(object? instance, Iterator iter)
{
try { GeneratedAction(instance, iter); }
catch { Console.Error.WriteLine(ReadableString); throw; }
}
public IterActionGenerator(World world, MethodInfo method)
{
World = world;
Method = method;
Parameters = method.GetParameters().Select(ParamInfo.Build).ToImmutableArray();
var name = "<>Query_" + string.Join("_", method.Name);
var genMethod = new DynamicMethod(name, null, new[] { typeof(object), typeof(Iterator) });
var IL = new ILGeneratorWrapper(genMethod);
var instanceArg = IL.Argument<object?>(0);
var iteratorArg = IL.Argument<Iterator>(1);
var fieldIndex = 0;
var paramData = new List<(ParamInfo Info, Term? Term, ILocal? FieldLocal, ILocal? TempLocal)>();
foreach (var p in Parameters) {
// If the parameter is unique, we don't create a term for it.
if (p.Kind <= ParamKind.Unique)
{ paramData.Add((p, null, null, null)); continue; }
// Create a term to add to the query.
var term = new Term(world.LookupByTypeOrThrow(p.UnderlyingType)) {
Source = (p.Source != null) ? (TermId)World.LookupByTypeOrThrow(p.Source) : null,
InOut = p.Kind switch {
ParamKind.In => TermInOutKind.In,
ParamKind.Out => TermInOutKind.Out,
ParamKind.Not or ParamKind.Not => TermInOutKind.None,
_ => default,
},
Oper = p.Kind switch {
ParamKind.Not => TermOperKind.Not,
ParamKind.Or => TermOperKind.Or,
_ when !p.IsRequired => TermOperKind.Optional,
_ => default,
},
};
// If this and the previous parameter are marked with [Or], do not advance the field index.
if ((fieldIndex == 0) || (p.Kind != ParamKind.Or) || (paramData[^1].Info.Kind != ParamKind.Or))
fieldIndex++;
var spanType = typeof(Span<>).MakeGenericType(p.FieldType);
var fieldLocal = (ILocal?)null;
var tempLocal = (ILocal?)null;
switch (p.Kind) {
// FIXME: Currently would not work with [Or]'d components.
case ParamKind.Has or ParamKind.Not or ParamKind.Or:
if (!p.ParameterType.IsValueType) break;
// If parameter is a struct, we require a temporary local that we can
// later load onto the stack when loading the arguments for the action.
IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});");
tempLocal = IL.Local(p.ParameterType);
IL.LoadAddr(tempLocal);
IL.Init(tempLocal.LocalType);
break;
case ParamKind.Nullable or ParamKind.Or:
IL.Comment($"{p.Info.Name}Field = iterator.FieldOrEmpty<{p.FieldType.Name}>({fieldIndex})");
fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field");
IL.Load(iteratorArg);
IL.LoadConst(fieldIndex);
IL.Call(_iteratorFieldOrEmptyMethod.MakeGenericMethod(p.FieldType));
IL.Store(fieldLocal);
if (p.UnderlyingType.IsValueType) {
IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});");
tempLocal = IL.Local(p.ParameterType);
IL.LoadAddr(tempLocal);
IL.Init(tempLocal.LocalType);
}
break;
default:
IL.Comment($"{p.Info.Name}Field = iterator.Field<{p.FieldType.Name}>({fieldIndex})");
fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field");
IL.Load(iteratorArg);
IL.LoadConst(fieldIndex);
IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType));
IL.Store(fieldLocal);
break;
}
paramData.Add((p, term, fieldLocal, tempLocal));
}
// If there's any reference type parameters, we need to define a GCHandle local.
var hasReferenceType = paramData
.Where(p => p.Info.Kind > ParamKind.Unique)
.Any(p => !p.Info.UnderlyingType.IsValueType);
var handleLocal = hasReferenceType ? IL.Local<GCHandle>() : null;
var indexLocal = IL.Local<int>("iter_index");
var countLocal = IL.Local<int>("iter_count");
IL.Set(indexLocal, 0);
IL.Load(iteratorArg, _iteratorCountProp);
IL.Store(countLocal);
// If all parameters are fixed, iterator count will be 0, but since
// the query matched, we want to run the callback at least once.
IL.Comment("if (iter_count == 0) iter_count = 1;");
var dontIncrementLabel = IL.DefineLabel();
IL.Load(countLocal);
IL.GotoIfTrue(dontIncrementLabel);
IL.LoadConst(1);
IL.Store(countLocal);
IL.MarkLabel(dontIncrementLabel);
IL.While("IteratorLoop", (@continue) => {
IL.GotoIf(@continue, indexLocal, Comparison.LessThan, countLocal);
}, (_, _) => {
if (!Method.IsStatic)
IL.Load(instanceArg);
foreach (var (info, term, fieldLocal, tempLocal) in paramData) {
var isValueType = info.UnderlyingType.IsValueType;
var paramName = info.ParameterType.GetFriendlyName();
switch (info.Kind) {
case ParamKind.GlobalUnique:
IL.Comment($"Global unique parameter {paramName}");
_globalUniqueParameters[info.ParameterType](IL, iteratorArg);
break;
case ParamKind.Unique:
IL.Comment($"Unique parameter {paramName}");
_uniqueParameters[info.ParameterType](IL, iteratorArg, indexLocal!);
break;
// FIXME: Currently would not work with [Or]'d components.
case ParamKind.Has or ParamKind.Not or ParamKind.Or:
IL.Comment($"Has parameter {paramName}");
if (isValueType) IL.LoadObj(tempLocal!);
else IL.LoadNull();
break;
default:
var spanType = typeof(Span<>).MakeGenericType(info.FieldType);
var spanItemMethod = spanType.GetProperty("Item")!.GetMethod!;
var spanLengthMethod = spanType.GetProperty("Length")!.GetMethod!;
IL.Comment($"Parameter {paramName}");
if (info.IsByRef) {
IL.LoadAddr(fieldLocal!);
if (info.IsFixed) IL.LoadConst(0);
else IL.Load(indexLocal!);
IL.Call(spanItemMethod);
} else if (info.IsRequired) {
IL.LoadAddr(fieldLocal!);
if (info.IsFixed) IL.LoadConst(0);
else IL.Load(indexLocal!);
IL.Call(spanItemMethod);
IL.LoadObj(info.FieldType);
if (!isValueType) {
IL.Comment($"Convert nint to {paramName}");
IL.Call(_handleFromIntPtrMethod);
IL.Store(handleLocal!);
IL.LoadAddr(handleLocal!);
IL.Call(_handleTargetProp.GetMethod!);
IL.Cast(info.UnderlyingType);
}
} else {
var elseLabel = IL.DefineLabel();
var doneLabel = IL.DefineLabel();
IL.LoadAddr(fieldLocal!);
IL.Call(spanLengthMethod);
IL.GotoIfFalse(elseLabel);
IL.LoadAddr(fieldLocal!);
if (info.IsFixed) IL.LoadConst(0);
else IL.Load(indexLocal!);
IL.Call(spanItemMethod);
IL.LoadObj(info.FieldType);
if (!isValueType) {
IL.Comment($"Convert nint to {paramName}");
IL.Call(_handleFromIntPtrMethod);
IL.Store(handleLocal!);
IL.LoadAddr(handleLocal!);
IL.Call(_handleTargetProp.GetMethod!);
IL.Cast(info.UnderlyingType);
} else IL.New(info.ParameterType);
IL.Goto(doneLabel);
IL.MarkLabel(elseLabel);
if (!isValueType) IL.LoadNull();
else IL.LoadObj(tempLocal!);
IL.MarkLabel(doneLabel);
}
break;
}
}
IL.Call(Method);
IL.Increment(indexLocal);
});
IL.Return();
Terms = paramData.Where(p => p.Term != null).Select(p => p.Term!).ToImmutableList();
GeneratedAction = genMethod.CreateDelegate<Action<object?, Iterator>>();
ReadableString = IL.ToReadableString();
}
public static IterActionGenerator GetOrBuild(World world, MethodInfo method)
=>_cache.GetValue(method, m => new IterActionGenerator(world, m));
public class ParamInfo
{
public ParameterInfo Info { get; }
public ParamKind Kind { get; }
public Type ParameterType { get; }
public Type UnderlyingType { get; }
public Type FieldType { get; }
public Type? Source { get; }
public bool IsRequired => (Kind < ParamKind.Nullable);
public bool IsByRef => (Kind is ParamKind.In or ParamKind.Ref);
public bool IsFixed => (Kind == ParamKind.GlobalUnique) || (Source != null);
private ParamInfo(ParameterInfo info, ParamKind kind,
Type paramType, Type underlyingType)
{
Info = info;
Kind = kind;
ParameterType = paramType;
UnderlyingType = underlyingType;
// Reference types have a backing type of nint - they're pointers.
FieldType = underlyingType.IsValueType ? underlyingType : typeof(nint);
if (UnderlyingType.Has<SingletonAttribute>()) Source = UnderlyingType;
if (Info.Get<SourceAttribute>()?.Type is Type type) Source = type;
}
public static ParamInfo Build(ParameterInfo info)
{
if (info.IsOptional) throw new ArgumentException($"Optional parameters are not supported\nParameter: {info}");
if (info.ParameterType.IsArray) throw new ArgumentException($"Arrays are not supported\nParameter: {info}");
if (info.ParameterType.IsPointer) throw new ArgumentException($"Pointers are not supported\nParameter: {info}");
if (info.ParameterType.IsPrimitive) throw new ArgumentException($"Primitives are not supported\nParameter: {info}");
// Find out initial parameter kind from provided attribute.
var fromAttributes = new List<ParamKind>();
if (info.Has< InAttribute>()) fromAttributes.Add(ParamKind.In);
if (info.Has<OutAttribute>()) fromAttributes.Add(ParamKind.Out);
if (info.Has<HasAttribute>()) fromAttributes.Add(ParamKind.Has);
if (info.Has< OrAttribute>()) fromAttributes.Add(ParamKind.Or);
if (info.Has<NotAttribute>()) fromAttributes.Add(ParamKind.Not);
// Throw an error if multiple incompatible attributes were found.
if (fromAttributes.Count > 1) throw new ArgumentException(
"Parameter must not be marked with multiple attributes: "
+ string.Join(", ", fromAttributes.Select(a => $"[{a}]"))
+ $"\nParameter: {info}");
var kind = fromAttributes.FirstOrNull() ?? ParamKind.Normal;
// Handle unique parameters such as Universe, EntityRef, ...
var isGlobalUnique = _globalUniqueParameters.ContainsKey(info.ParameterType);
var isUnique = _uniqueParameters.ContainsKey(info.ParameterType);
if (isGlobalUnique || isUnique) {
if (kind != ParamKind.Normal) throw new ArgumentException(
$"Unique parameter {info.ParameterType.Name} does not support [{kind}]\nParameter: {info}");
kind = isGlobalUnique ? ParamKind.GlobalUnique : ParamKind.Unique;
return new(info, kind, info.ParameterType, info.ParameterType);
}
var isNullable = info.IsNullable();
var isByRef = info.ParameterType.IsByRef;
if (info.ParameterType.Has<TagAttribute>() && (kind is not (ParamKind.Has or ParamKind.Not or ParamKind.Or))) {
if (kind is not ParamKind.Normal) throw new ArgumentException($"Parameter does not support [{kind}]\nParameter: {info}");
kind = ParamKind.Has;
}
if (kind is ParamKind.Not or ParamKind.Has) {
if (isNullable) throw new ArgumentException($"Parameter does not support Nullable\nParameter: {info}");
if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}");
return new(info, kind, info.ParameterType, info.ParameterType);
}
var underlyingType = info.ParameterType;
if (isNullable) {
if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}");
if (info.ParameterType.IsValueType)
underlyingType = Nullable.GetUnderlyingType(info.ParameterType)!;
kind = ParamKind.Nullable;
}
if (info.ParameterType.IsByRef) {
if (kind != ParamKind.Normal) throw new ArgumentException(
$"Parameter does not support [{kind}]\nParameter: {info}");
underlyingType = info.ParameterType.GetElementType()!;
if (!underlyingType.IsValueType) throw new ArgumentException(
$"Reference types can't also be ByRef\nParameter: {info}");
kind = info.IsIn ? ParamKind.In
: info.IsOut ? ParamKind.Out
: ParamKind.Ref;
}
if (underlyingType.IsPrimitive) throw new ArgumentException(
$"Primitives are not supported\nParameter: {info}");
return new(info, kind, info.ParameterType, underlyingType);
}
}
public enum ParamKind
{
/// <summary>
/// Not part of the resulting query's terms.
/// Same value across a single invocation of a callback.
/// For example <see cref="ECS.World"/> or <see cref="TimeSpan"/>.
/// </summary>
GlobalUnique,
/// <summary>
/// Not part of the resulting query's terms.
/// Unique value for each iterated entity.
/// For example <see cref="EntityRef"/>.
/// </summary>
Unique,
/// <summary> Passed by value. </summary>
Normal,
/// <summary>
/// Struct passed with the "in" modifier, allowing direct pointer access.
/// Manually applied with <see cref="InAttribute"/>.
/// Marks a component as being read from.
/// </summary>
In,
/// <summary>
/// Struct passed with the "out" modifier, allowing direct pointer access.
/// Manually applied with <see cref="HasAttribute"/>.
/// Marks a component as being written to.
/// </summary>
Out,
/// <summary>
/// Struct passed with the "ref" modifier, allowing direct pointer access.
/// Marks a component as being read from and written to.
/// </summary>
Ref,
/// <summary>
/// Only checks for presence.
/// Manually applied with <see cref="HasAttribute"/>.
/// Automatically applied for types with <see cref="TagAttribute"/>.
/// Marks a component as not being accessed.
/// </summary>
Has,
/// <summary>
/// Struct or class passed as <see cref="T?"/>.
/// </summary>
Nullable,
/// <summary>
/// Only checks for absence.
/// Applied with <see cref="NotAttribute"/>.
/// </summary>
Not,
/// <summary>
/// Matches any terms in a chain of "or" terms.
/// Applied with <see cref="OrAttribute"/>.
/// Implies <see cref="Nullable"/>.
/// </summary>
Or,
}
}