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; }
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);
return this;
@ -88,7 +88,7 @@ public class EntityBuilder
if (Path != null) {
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.Count > 1) parent = EntityPath.EnsureEntityExists(Universe, parent, Path.Parent!);
}

@ -74,7 +74,7 @@ public static class SystemExtensions
var param = method.GetParameters();
if (param.Length == 1 && param[0].ParameterType == typeof(Iterator)) {
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);
} else {
var gen = IterActionGenerator.GetOrBuild(universe, method);
@ -116,8 +116,7 @@ public static class SystemExtensions
callback.Prepare(iter);
var query = flecsIter->priv.iter.query.query;
if (query != null && ecs_query_get_filter(query)->term_count == 0)
if (flecsIter->field_count == 0)
callback.Callback(iter);
else while (iter.Next())
callback.Callback(iter);

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

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
namespace gaemstone.Utility;
@ -11,4 +12,11 @@ public static class CollectionExtensions
where T : struct => MaybeGet((ReadOnlySpan<T>)span, index);
public static T? MaybeGet<T>(this ReadOnlySpan<T> span, int index)
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.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using gaemstone.ECS;
using GCHandle = System.Runtime.InteropServices.GCHandle;
namespace gaemstone.Utility.IL;
// TODO: Implement "or" operator.
// TODO: Support tuple syntax to match relationship pairs.
public unsafe class IterActionGenerator
{
@ -63,7 +62,7 @@ public unsafe class IterActionGenerator
var instanceArg = IL.Argument<object?>(0);
var iteratorArg = IL.Argument<Iterator>(1);
var fieldIndex = 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.
@ -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 fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field");
var fieldLocal = (ILocal?)null;
var tempLocal = (ILocal?)null;
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 a "has" or "not" parameter is a struct, we require a temporary local that
// we can later load onto the stack when loading the arguments for the action.
// 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:
case ParamKind.Nullable or ParamKind.Or:
IL.Comment($"{p.Info.Name}Field = iterator.MaybeField<{p.FieldType.Name}>({fieldIndex})");
fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field");
IL.Load(iteratorArg);
IL.LoadConst(fieldIndex);
IL.Call(_iteratorMaybeFieldMethod.MakeGenericMethod(p.FieldType));
@ -117,6 +122,7 @@ public unsafe class IterActionGenerator
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));
@ -125,7 +131,6 @@ public unsafe class IterActionGenerator
}
paramData.Add((p, term, fieldLocal, tempLocal));
fieldIndex++;
}
// 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);
var handleLocal = hasReferenceType ? IL.Local<GCHandle>() : null;
var countLocal = IL.Local<int>("iter_count");
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);
IL.Set(indexLocal, 0);
// If all parameters are fixed, iterator count will be 0, but since
// the query matched, we want to run the callback at least once.
@ -158,6 +163,7 @@ public unsafe class IterActionGenerator
IL.Load(instanceArg);
foreach (var (info, term, fieldLocal, tempLocal) in paramData) {
var isValueType = info.UnderlyingType.IsValueType;
switch (info.Kind) {
case ParamKind.GlobalUnique:
@ -170,10 +176,10 @@ public unsafe class IterActionGenerator
_uniqueParameters[info.ParameterType](IL, iteratorArg, indexLocal!);
break;
case ParamKind.Has or ParamKind.Not:
if (info.ParameterType.IsValueType)
IL.LoadObj(tempLocal!);
else IL.LoadNull();
// FIXME: Currently would not work with [Or]'d components.
case ParamKind.Has or ParamKind.Not or ParamKind.Or:
if (isValueType) IL.LoadObj(tempLocal!);
else IL.LoadNull();
break;
default:
@ -214,7 +220,7 @@ public unsafe class IterActionGenerator
IL.MarkLabel(doneLabel);
}
if (!info.UnderlyingType.IsValueType) {
if (!isValueType) {
IL.Comment($"Convert nint to {info.UnderlyingType.GetFriendlyName()}");
IL.Call(_handleFromIntPtrMethod);
IL.Store(handleLocal!);
@ -252,7 +258,7 @@ public unsafe class IterActionGenerator
public Type? Source { get; }
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);
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.
FieldType = underlyingType.IsValueType ? underlyingType : typeof(nint);
if (UnderlyingType.Has<SingletonAttribute>()) Source = underlyingType;
if (Info.Get<SourceAttribute>() is SourceAttribute attr) Source = attr.Type;
// TODO: Needs support for the new attributes.
Source = Info.Get<SourceAttribute>()?.Type
?? (UnderlyingType.Has<SingletonAttribute>() ? UnderlyingType : null);
}
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 (_globalUniqueParameters.ContainsKey(info.ParameterType))
return new(info, ParamKind.GlobalUnique, info.ParameterType, info.ParameterType);
if (_uniqueParameters.ContainsKey(info.ParameterType))
return new(info, ParamKind.Unique, info.ParameterType, info.ParameterType);
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 isByRef = info.ParameterType.IsByRef;
var isNullable = info.IsNullable();
var isByRef = info.ParameterType.IsByRef;
if (info.Has<NotAttribute>()) {
if (isByRef || isNullable) throw new ArgumentException(
"Parameter with NotAttribute must not be ByRef or nullable\nParameter: " + info);
return new(info, ParamKind.Not, info.ParameterType, info.ParameterType);
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 (info.Has<HasAttribute>() || info.ParameterType.Has<TagAttribute>()) {
if (isByRef || isNullable) throw new ArgumentException(
"Parameter with HasAttribute / TagAttribute must not be ByRef or nullable\nParameter: " + info);
return new(info, ParamKind.Has, info.ParameterType, info.ParameterType);
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 kind = ParamKind.Normal;
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)
underlyingType = Nullable.GetUnderlyingType(info.ParameterType)!;
kind = ParamKind.Nullable;
}
if (info.ParameterType.IsByRef) {
if (kind == ParamKind.Nullable) throw new ArgumentException(
"ByRef and Nullable are not supported together\nParameter: " + info);
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);
$"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);
$"Primitives are not supported\nParameter: {info}");
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"/>.
/// </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. </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,
/// <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,
/// <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,
/// <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>
/// Matches any terms in a chain of "or" terms.
/// Applied with <see cref="OrAttribute"/>.
/// Implies <see cref="Nullable"/>.
/// Struct or class passed as <see cref="T?"/>.
/// </summary>
Or,
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,
}
}

Loading…
Cancel
Save