Support [Or] and other attributes

(currently only tags, not components)
wip/source-generators
copygirl 2 years ago
parent 129f75649c
commit c4067255e9
  1. 4
      src/gaemstone/ECS/EntityBuilder.cs
  2. 5
      src/gaemstone/ECS/System.cs
  3. 14
      src/gaemstone/ECS/TermAttributes.cs
  4. 8
      src/gaemstone/Utility/CollectionExtensions.cs
  5. 151
      src/gaemstone/Utility/IL/IterActionGenerator.cs

@ -56,7 +56,7 @@ public class EntityBuilder
relation == Universe.ChildOf) { _parent = target; return this; } relation == Universe.ChildOf) { _parent = target; return this; }
if (_toAdd.Count == 31) throw new NotSupportedException( if (_toAdd.Count == 31) throw new NotSupportedException(
$"Must not add more than 31 IDs at once with EntityBuilder"); "Must not add more than 31 IDs at once with EntityBuilder");
_toAdd.Add(id); _toAdd.Add(id);
return this; return this;
@ -88,7 +88,7 @@ public class EntityBuilder
if (Path != null) { if (Path != null) {
if (parent.IsSome && Path.IsAbsolute) throw new InvalidOperationException( if (parent.IsSome && Path.IsAbsolute) throw new InvalidOperationException(
$"Entity already has parent set (via ChildOf), so path must not be absolute"); "Entity already has parent set (via ChildOf), so path must not be absolute");
// If path specifies more than just a name, ensure the parent entity exists. // If path specifies more than just a name, ensure the parent entity exists.
if (Path.Count > 1) parent = EntityPath.EnsureEntityExists(Universe, parent, Path.Parent!); if (Path.Count > 1) parent = EntityPath.EnsureEntityExists(Universe, parent, Path.Parent!);
} }

@ -74,7 +74,7 @@ public static class SystemExtensions
var param = method.GetParameters(); var param = method.GetParameters();
if (param.Length == 1 && param[0].ParameterType == typeof(Iterator)) { if (param.Length == 1 && param[0].ParameterType == typeof(Iterator)) {
query = new(expr ?? throw new ArgumentException( query = new(expr ?? throw new ArgumentException(
"System must specify SystemAttribute.Expression", nameof(method))); "System must specify ExpressionAttribute", nameof(method)));
callback = (Action<Iterator>)Delegate.CreateDelegate(typeof(Action<Iterator>), instance, method); callback = (Action<Iterator>)Delegate.CreateDelegate(typeof(Action<Iterator>), instance, method);
} else { } else {
var gen = IterActionGenerator.GetOrBuild(universe, method); var gen = IterActionGenerator.GetOrBuild(universe, method);
@ -116,8 +116,7 @@ public static class SystemExtensions
callback.Prepare(iter); callback.Prepare(iter);
var query = flecsIter->priv.iter.query.query; if (flecsIter->field_count == 0)
if (query != null && ecs_query_get_filter(query)->term_count == 0)
callback.Callback(iter); callback.Callback(iter);
else while (iter.Next()) else while (iter.Next())
callback.Callback(iter); callback.Callback(iter);

@ -2,12 +2,12 @@ using System;
namespace gaemstone.ECS; namespace gaemstone.ECS;
// TODO: Make it possible to use [Source] on systems. [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)]
[AttributeUsage(AttributeTargets.Parameter)]
public class SourceAttribute : Attribute public class SourceAttribute : Attribute
{ {
public Type Type { get; } public Type Type { get; }
public SourceAttribute(Type type) => Type = type; // TODO: Support path as source too.
internal SourceAttribute(Type type) => Type = type;
} }
public class SourceAttribute<TEntity> : SourceAttribute public class SourceAttribute<TEntity> : SourceAttribute
{ public SourceAttribute() : base(typeof(TEntity)) { } } { public SourceAttribute() : base(typeof(TEntity)) { } }
@ -25,12 +25,8 @@ public class InAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Parameter)] [AttributeUsage(AttributeTargets.Parameter)]
public class OutAttribute : Attribute { } public class OutAttribute : Attribute { }
// [AttributeUsage(AttributeTargets.Parameter)]
// public class OrAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Parameter)] [AttributeUsage(AttributeTargets.Parameter)]
public class NotAttribute : Attribute { } public class OrAttribute : Attribute { }
// Parameters with nullable syntax are equivalent to [Optional].
[AttributeUsage(AttributeTargets.Parameter)] [AttributeUsage(AttributeTargets.Parameter)]
public class OptionalAttribute : Attribute { } public class NotAttribute : Attribute { }

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace gaemstone.Utility; namespace gaemstone.Utility;
@ -11,4 +12,11 @@ public static class CollectionExtensions
where T : struct => MaybeGet((ReadOnlySpan<T>)span, index); where T : struct => MaybeGet((ReadOnlySpan<T>)span, index);
public static T? MaybeGet<T>(this ReadOnlySpan<T> span, int index) public static T? MaybeGet<T>(this ReadOnlySpan<T> span, int index)
where T : struct => (index >= 0 && index < span.Length) ? span[index] : null; where T : struct => (index >= 0 && index < span.Length) ? span[index] : null;
public static T? FirstOrNull<T>(this IEnumerable<T> enumerable)
where T : struct
{
using var enumerator = enumerable.GetEnumerator();
return enumerator.MoveNext() ? enumerator.Current : null;
}
} }

@ -5,12 +5,11 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Reflection.Emit; using System.Reflection.Emit;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using gaemstone.ECS; using gaemstone.ECS;
using GCHandle = System.Runtime.InteropServices.GCHandle;
namespace gaemstone.Utility.IL; namespace gaemstone.Utility.IL;
// TODO: Implement "or" operator.
// TODO: Support tuple syntax to match relationship pairs. // TODO: Support tuple syntax to match relationship pairs.
public unsafe class IterActionGenerator public unsafe class IterActionGenerator
{ {
@ -63,7 +62,7 @@ public unsafe class IterActionGenerator
var instanceArg = IL.Argument<object?>(0); var instanceArg = IL.Argument<object?>(0);
var iteratorArg = IL.Argument<Iterator>(1); var iteratorArg = IL.Argument<Iterator>(1);
var fieldIndex = 1; var fieldIndex = 0;
var paramData = new List<(ParamInfo Info, Term? Term, ILocal? FieldLocal, ILocal? TempLocal)>(); var paramData = new List<(ParamInfo Info, Term? Term, ILocal? FieldLocal, ILocal? TempLocal)>();
foreach (var p in Parameters) { foreach (var p in Parameters) {
// If the parameter is unique, we don't create a term for it. // If the parameter is unique, we don't create a term for it.
@ -87,23 +86,29 @@ public unsafe class IterActionGenerator
}, },
}; };
// 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 spanType = typeof(Span<>).MakeGenericType(p.FieldType);
var fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field"); var fieldLocal = (ILocal?)null;
var tempLocal = (ILocal?)null; var tempLocal = (ILocal?)null;
switch (p.Kind) { switch (p.Kind) {
case ParamKind.Has or ParamKind.Not: // FIXME: Currently would not work with [Or]'d components.
case ParamKind.Has or ParamKind.Not or ParamKind.Or:
if (!p.ParameterType.IsValueType) break; if (!p.ParameterType.IsValueType) break;
// If a "has" or "not" parameter is a struct, we require a temporary local that // If parameter is a struct, we require a temporary local that we can
// we can later load onto the stack when loading the arguments for the action. // later load onto the stack when loading the arguments for the action.
IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});"); IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});");
tempLocal = IL.Local(p.ParameterType); tempLocal = IL.Local(p.ParameterType);
IL.LoadAddr(tempLocal); IL.LoadAddr(tempLocal);
IL.Init(tempLocal.LocalType); IL.Init(tempLocal.LocalType);
break; break;
case ParamKind.Nullable: case ParamKind.Nullable or ParamKind.Or:
IL.Comment($"{p.Info.Name}Field = iterator.MaybeField<{p.FieldType.Name}>({fieldIndex})"); IL.Comment($"{p.Info.Name}Field = iterator.MaybeField<{p.FieldType.Name}>({fieldIndex})");
fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field");
IL.Load(iteratorArg); IL.Load(iteratorArg);
IL.LoadConst(fieldIndex); IL.LoadConst(fieldIndex);
IL.Call(_iteratorMaybeFieldMethod.MakeGenericMethod(p.FieldType)); IL.Call(_iteratorMaybeFieldMethod.MakeGenericMethod(p.FieldType));
@ -117,6 +122,7 @@ public unsafe class IterActionGenerator
default: default:
IL.Comment($"{p.Info.Name}Field = iterator.Field<{p.FieldType.Name}>({fieldIndex})"); IL.Comment($"{p.Info.Name}Field = iterator.Field<{p.FieldType.Name}>({fieldIndex})");
fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field");
IL.Load(iteratorArg); IL.Load(iteratorArg);
IL.LoadConst(fieldIndex); IL.LoadConst(fieldIndex);
IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType)); IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType));
@ -125,7 +131,6 @@ public unsafe class IterActionGenerator
} }
paramData.Add((p, term, fieldLocal, tempLocal)); paramData.Add((p, term, fieldLocal, tempLocal));
fieldIndex++;
} }
// If there's any reference type parameters, we need to define a GCHandle local. // If there's any reference type parameters, we need to define a GCHandle local.
@ -134,12 +139,12 @@ public unsafe class IterActionGenerator
.Any(p => !p.Info.UnderlyingType.IsValueType); .Any(p => !p.Info.UnderlyingType.IsValueType);
var handleLocal = hasReferenceType ? IL.Local<GCHandle>() : null; var handleLocal = hasReferenceType ? IL.Local<GCHandle>() : null;
var countLocal = IL.Local<int>("iter_count");
var indexLocal = IL.Local<int>("iter_index"); var indexLocal = IL.Local<int>("iter_index");
var countLocal = IL.Local<int>("iter_count");
IL.Set(indexLocal, 0);
IL.Load(iteratorArg, _iteratorCountProp); IL.Load(iteratorArg, _iteratorCountProp);
IL.Store(countLocal); IL.Store(countLocal);
IL.Set(indexLocal, 0);
// If all parameters are fixed, iterator count will be 0, but since // If all parameters are fixed, iterator count will be 0, but since
// the query matched, we want to run the callback at least once. // the query matched, we want to run the callback at least once.
@ -158,6 +163,7 @@ public unsafe class IterActionGenerator
IL.Load(instanceArg); IL.Load(instanceArg);
foreach (var (info, term, fieldLocal, tempLocal) in paramData) { foreach (var (info, term, fieldLocal, tempLocal) in paramData) {
var isValueType = info.UnderlyingType.IsValueType;
switch (info.Kind) { switch (info.Kind) {
case ParamKind.GlobalUnique: case ParamKind.GlobalUnique:
@ -170,10 +176,10 @@ public unsafe class IterActionGenerator
_uniqueParameters[info.ParameterType](IL, iteratorArg, indexLocal!); _uniqueParameters[info.ParameterType](IL, iteratorArg, indexLocal!);
break; break;
case ParamKind.Has or ParamKind.Not: // FIXME: Currently would not work with [Or]'d components.
if (info.ParameterType.IsValueType) case ParamKind.Has or ParamKind.Not or ParamKind.Or:
IL.LoadObj(tempLocal!); if (isValueType) IL.LoadObj(tempLocal!);
else IL.LoadNull(); else IL.LoadNull();
break; break;
default: default:
@ -214,7 +220,7 @@ public unsafe class IterActionGenerator
IL.MarkLabel(doneLabel); IL.MarkLabel(doneLabel);
} }
if (!info.UnderlyingType.IsValueType) { if (!isValueType) {
IL.Comment($"Convert nint to {info.UnderlyingType.GetFriendlyName()}"); IL.Comment($"Convert nint to {info.UnderlyingType.GetFriendlyName()}");
IL.Call(_handleFromIntPtrMethod); IL.Call(_handleFromIntPtrMethod);
IL.Store(handleLocal!); IL.Store(handleLocal!);
@ -252,7 +258,7 @@ public unsafe class IterActionGenerator
public Type? Source { get; } public Type? Source { get; }
public bool IsRequired => (Kind < ParamKind.Nullable); public bool IsRequired => (Kind < ParamKind.Nullable);
public bool IsByRef => (Kind >= ParamKind.In) && (Kind <= ParamKind.Ref); public bool IsByRef => (Kind is ParamKind.In or ParamKind.Ref);
public bool IsFixed => (Kind == ParamKind.GlobalUnique) || (Source != null); public bool IsFixed => (Kind == ParamKind.GlobalUnique) || (Source != null);
private ParamInfo(ParameterInfo info, ParamKind kind, private ParamInfo(ParameterInfo info, ParamKind kind,
@ -265,59 +271,77 @@ public unsafe class IterActionGenerator
// Reference types have a backing type of nint - they're pointers. // Reference types have a backing type of nint - they're pointers.
FieldType = underlyingType.IsValueType ? underlyingType : typeof(nint); FieldType = underlyingType.IsValueType ? underlyingType : typeof(nint);
if (UnderlyingType.Has<SingletonAttribute>()) Source = underlyingType; Source = Info.Get<SourceAttribute>()?.Type
if (Info.Get<SourceAttribute>() is SourceAttribute attr) Source = attr.Type; ?? (UnderlyingType.Has<SingletonAttribute>() ? UnderlyingType : null);
// TODO: Needs support for the new attributes.
} }
public static ParamInfo Build(ParameterInfo info) public static ParamInfo Build(ParameterInfo info)
{ {
if (info.IsOptional) throw new ArgumentException("Optional parameters are not supported\nParameter: " + 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.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.IsPointer) throw new ArgumentException($"Pointers are not supported\nParameter: {info}");
if (info.ParameterType.IsPrimitive) throw new ArgumentException($"Primitives are not supported\nParameter: {info}");
if (_globalUniqueParameters.ContainsKey(info.ParameterType))
return new(info, ParamKind.GlobalUnique, info.ParameterType, info.ParameterType); // Find out initial parameter kind from provided attribute.
if (_uniqueParameters.ContainsKey(info.ParameterType)) var fromAttributes = new List<ParamKind>();
return new(info, ParamKind.Unique, info.ParameterType, info.ParameterType); 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 isByRef = info.ParameterType.IsByRef;
var isNullable = info.IsNullable(); var isNullable = info.IsNullable();
var isByRef = info.ParameterType.IsByRef;
if (info.Has<NotAttribute>()) { if (info.ParameterType.Has<TagAttribute>() && (kind is not (ParamKind.Has or ParamKind.Not or ParamKind.Or))) {
if (isByRef || isNullable) throw new ArgumentException( if (kind is not ParamKind.Normal) throw new ArgumentException($"Parameter does not support [{kind}]\nParameter: {info}");
"Parameter with NotAttribute must not be ByRef or nullable\nParameter: " + info); kind = ParamKind.Has;
return new(info, ParamKind.Not, info.ParameterType, info.ParameterType);
} }
if (info.Has<HasAttribute>() || info.ParameterType.Has<TagAttribute>()) { if (kind is ParamKind.Not or ParamKind.Has) {
if (isByRef || isNullable) throw new ArgumentException( if (isNullable) throw new ArgumentException($"Parameter does not support Nullable\nParameter: {info}");
"Parameter with HasAttribute / TagAttribute must not be ByRef or nullable\nParameter: " + info); if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}");
return new(info, ParamKind.Has, info.ParameterType, info.ParameterType); return new(info, kind, info.ParameterType, info.ParameterType);
} }
var kind = ParamKind.Normal;
var underlyingType = info.ParameterType; var underlyingType = info.ParameterType;
if (info.IsNullable()) { if (isNullable) {
if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}");
if (info.ParameterType.IsValueType) if (info.ParameterType.IsValueType)
underlyingType = Nullable.GetUnderlyingType(info.ParameterType)!; underlyingType = Nullable.GetUnderlyingType(info.ParameterType)!;
kind = ParamKind.Nullable; kind = ParamKind.Nullable;
} }
if (info.ParameterType.IsByRef) { if (info.ParameterType.IsByRef) {
if (kind == ParamKind.Nullable) throw new ArgumentException( if (kind != ParamKind.Normal) throw new ArgumentException(
"ByRef and Nullable are not supported together\nParameter: " + info); $"Parameter does not support [{kind}]\nParameter: {info}");
underlyingType = info.ParameterType.GetElementType()!; underlyingType = info.ParameterType.GetElementType()!;
if (!underlyingType.IsValueType) throw new ArgumentException( if (!underlyingType.IsValueType) throw new ArgumentException(
"Reference types can't also be ByRef\nParameter: " + info); $"Reference types can't also be ByRef\nParameter: {info}");
kind = info.IsIn ? ParamKind.In kind = info.IsIn ? ParamKind.In
: info.IsOut ? ParamKind.Out : info.IsOut ? ParamKind.Out
: ParamKind.Ref; : ParamKind.Ref;
} }
if (underlyingType.IsPrimitive) throw new ArgumentException( if (underlyingType.IsPrimitive) throw new ArgumentException(
"Primitives are not supported\nParameter: " + info); $"Primitives are not supported\nParameter: {info}");
return new(info, kind, info.ParameterType, underlyingType); return new(info, kind, info.ParameterType, underlyingType);
} }
@ -331,38 +355,61 @@ public unsafe class IterActionGenerator
/// For example <see cref="ECS.Universe"/> or <see cref="TimeSpan"/>. /// For example <see cref="ECS.Universe"/> or <see cref="TimeSpan"/>.
/// </summary> /// </summary>
GlobalUnique, GlobalUnique,
/// <summary> /// <summary>
/// Not part of the resulting query's terms. /// Not part of the resulting query's terms.
/// Unique value for each iterated entity. /// Unique value for each iterated entity.
/// For example <see cref="EntityRef"/>. /// For example <see cref="EntityRef"/>.
/// </summary> /// </summary>
Unique, Unique,
/// <summary> Passed by value. </summary> /// <summary> Passed by value. </summary>
Normal, Normal,
/// <summary> Struct passed with the "in" modifier. </summary>
/// <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, In,
/// <summary> Struct passed with the "out" modifier. </summary>
/// <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, Out,
/// <summary> Struct passed with the "ref" modifier. </summary>
/// <summary>
/// Struct passed with the "ref" modifier, allowing direct pointer access.
/// Marks a component as being read from and written to.
/// </summary>
Ref, Ref,
/// <summary> /// <summary>
/// Only checks for presence. /// Only checks for presence.
/// Manually applied with <see cref="HasAttribute"/>. /// Manually applied with <see cref="HasAttribute"/>.
/// Automatically applied for types with <see cref="TagAttribute"/>. /// Automatically applied for types with <see cref="TagAttribute"/>.
/// Marks a component as not being accessed.
/// </summary> /// </summary>
Has, Has,
/// <summary> Struct or class passed as <see cref="T?"/>. </summary>
Nullable,
/// <summary> /// <summary>
/// Matches any terms in a chain of "or" terms. /// Struct or class passed as <see cref="T?"/>.
/// Applied with <see cref="OrAttribute"/>.
/// Implies <see cref="Nullable"/>.
/// </summary> /// </summary>
Or, Nullable,
/// <summary> /// <summary>
/// Only checks for absence. /// Only checks for absence.
/// Applied with <see cref="NotAttribute"/>. /// Applied with <see cref="NotAttribute"/>.
/// </summary> /// </summary>
Not, Not,
/// <summary>
/// Matches any terms in a chain of "or" terms.
/// Applied with <see cref="OrAttribute"/>.
/// Implies <see cref="Nullable"/>.
/// </summary>
Or,
} }
} }

Loading…
Cancel
Save