qamp/Qrakhen.Qamp.Core/Values/ExtensionMethod.cs

261 lines
9.2 KiB
C#

using Qrakhen.Qamp.Core.Collections;
using Qrakhen.Qamp.Core.Logging;
using Qrakhen.Qamp.Core.Values.Objects;
using System.Linq.Expressions;
using System.Reflection;
namespace Qrakhen.Qamp.Core.Values;
public delegate Value ExtensionDelegate(Value target, Value[] parameters);
public class ExtensionException(string message, object? context = null) : QampException(message, context);
public abstract class ExtensionAttribute : Attribute;
public abstract class TypedExtensionAttribute : ExtensionAttribute
{
public ValueType ValueType { get; set; } = ValueType.Undefined;
public Type? ValueObjectType { get; set; } = null;
public string? ValueObjectName { get; set; }
public bool Nullable { get; set; } = false;
public TypeInfo TypeInfo => new TypeInfo(ValueType, ValueObjectType, ValueObjectName, Nullable);
}
[AttributeUsage(AttributeTargets.Method)]
public class ExtensionMethodAttribute : ExtensionAttribute
{
public string? Name { get; set; } = null;
}
[AttributeUsage(AttributeTargets.Method)]
public class ReturnsAttribute : TypedExtensionAttribute;
[AttributeUsage(AttributeTargets.Parameter)]
public class SelfAttribute : TypedExtensionAttribute;
[AttributeUsage(AttributeTargets.Class)]
public class ExtensionClassAttribute : SelfAttribute
{
public string? Name { get; set; } = null;
}
[AttributeUsage(AttributeTargets.Parameter)]
public class ParameterAttribute : TypedExtensionAttribute
{
public string? Name { get; set; } = null;
public bool Optional { get; set; } = false;
}
public class ExtensionMethod(
TypeInfo targetType,
TypeInfo returnType,
string name,
ExtensionDelegate callback,
string[] parameters)
{
private static readonly Register<TypeInfo, Register<string, ExtensionMethod>> _register = [];
private static readonly ILogger _logger = LoggerService.Get<ExtensionMethod>();
public readonly string Name = name;
public readonly TypeInfo TargetType = targetType;
public readonly TypeInfo ReturnType = returnType;
public readonly ExtensionDelegate Callback = callback;
public readonly string[] Parameters = parameters.ToArray();
public readonly string Key = parameters.Length > 0 ? $"{name}_{parameters.Length}" : name;
public static ExtensionMethod? Get(TypeInfo targetType, string name, int parameters = 0)
{
if (parameters > 0)
name = $"{name}_{parameters}";
if (!_register.Has(targetType))
return null;
if (!_register[targetType].Has(name))
return null;
return _register[targetType][name];
}
public static void Register(IEnumerable<ExtensionMethod> extensions)
{
foreach (var extension in extensions)
Register(extension);
}
public static void Register(ExtensionMethod extension)
{
if (!_register.Has(extension.TargetType))
_register.Add(extension.TargetType, []);
if (_register[extension.TargetType].Has(extension.Key))
throw new ExtensionException($"Extension {extension.Key} already exists for type {extension.TargetType}.");
_register[extension.TargetType].Add(extension.Key, extension);
}
public static void Register(TypeInfo targetType,
TypeInfo returnType,
string name,
ExtensionDelegate callback,
params string[] parameters)
=> Register(new ExtensionMethod(targetType, returnType, name, callback, parameters));
private static ExtensionMethod RenderExtension(MethodInfo method)
{
_logger.Verbose($"Rendering extension from {method}...");
var attr = method.GetCustomAttribute<ExtensionMethodAttribute>();
if (attr is null)
throw new ExtensionException($"Expected an [ExtensionMethod] attribute on method {method} for extension compilation.", method);
var parameters = method.GetParameters();
if (parameters.Length < 1)
throw new ExtensionException($"Tried to define method {method} as extension, but extensions require their first parameter to be <self>, the target value of the extension.", method);
var selfAttr = parameters[0].GetCustomAttribute<SelfAttribute>() ?? method.DeclaringType?.GetCustomAttribute<ExtensionClassAttribute>();
if (selfAttr is null)
throw new ExtensionException($"Expected first parameter of {method} to have a [Self], or its declaring type to have a [ExtensionClass] attribute in order to define the extension's target type.");
TypeInfo anyType = TypeInfo.Any;
string name = attr.Name ?? method.Name;
TypeInfo selfType = selfAttr.TypeInfo;
TypeInfo returnType = method.GetCustomAttribute<ReturnsAttribute>()?.TypeInfo ?? anyType; // todo: assume returned type from actual return value
string[] parameterNames = parameters.Subset(1).Select(p => p.Name!).ToArray() ?? []; // todo: use the actual parameter attribute for this.
ExtensionDelegate callback = RenderDelegate(method);
return new ExtensionMethod(selfType, returnType, name, callback, parameterNames);
}
private static ExtensionDelegate RenderDelegate(MethodInfo method)
{
_logger.Verbose($"Rendering delegate expression...");
var selfParameter = Expression.Parameter(typeof(Value), "self");
var argsParameter = Expression.Parameter(typeof(Value[]), "args");
ParameterInfo[] parameterInfos = method.GetParameters();
var parameterExpressions = new Expression[parameterInfos.Length - 1];
for (int i = 0; i < parameterExpressions.Length; i++) {
ConstantExpression index = Expression.Constant(i);
BinaryExpression indexExpression = Expression.ArrayIndex(argsParameter, index);
/* todo: unsure
Type requiredType = parameterInfos[i].ParameterType;
if (indexExpression.Type != requiredType) {
parameterExpressions[i] = Expression.Convert(indexExpression, requiredType);
}*/
parameterExpressions[i] = indexExpression;
}
Expression[] arguments = [selfParameter, .. parameterExpressions];
MethodCallExpression callExpression = Expression.Call(method, arguments);
Expression final;
if (method.ReturnType != typeof(Value)) {
ConstructorInfo? ctor = typeof(Value).GetConstructor([ method.ReturnType ]);
if (ctor is null)
throw new ExtensionException($"Can not compile extension from method {method}, its return type '{method.ReturnType}' is not accepted by any Value constructors.", method);
final = Expression.New(ctor, callExpression);
} else {
final = callExpression;
}
var lambda = Expression.Lambda<ExtensionDelegate>(final, selfParameter, argsParameter);
_logger.Verbose($"Done: ", lambda);
return lambda.Compile();
}
public static IEnumerable<ExtensionMethod> CompileExtensionsFromType(Type type)
{
_logger.Debug($"Compiling static extension methods from {type}...");
List<ExtensionMethod> extensions = [];
var methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public)
.Where(method => method.GetCustomAttribute<ExtensionMethodAttribute>() is not null)
.ToArray();
if (methods.Length == 0) {
_logger.Warn($"Tried to compile static extension methods from {type}, but no public static methods were found declared inside it.");
return extensions;
}
foreach (var method in methods) {
extensions.Add(RenderExtension(method));
}
return extensions;
}
static ExtensionMethod()
{
Register(CompileExtensionsFromType(typeof(StringExtensions)));
}
public Value Call(Value self, Value[] parameters)
{
return Callback(self, parameters);
}
public override string ToString()
{
return $"{TargetType}.{Name}({string.Join(", ", Parameters)}) (NativeExtension)";
}
}
[ExtensionClass(ValueType = ValueType.String)]
public static class StringExtensions
{
[ExtensionMethod]
public static long Length(Value self) // make this possible with Objects.String and auto-detection
{
return self.Ptr.As<Objects.String>()?.Value?.Length ?? 0;
}
[ExtensionMethod]
public static long IndexOf(Value self, Value needle)
{
string _self = self.Ptr.As<Objects.String>()?.Value ?? "";
string _needle = self.Ptr.As<Objects.String>()?.Value ?? "";
if (_needle.Length < 1 || _self.Length < 1)
return -1;
return _self.IndexOf(_needle);
}
[ExtensionMethod]
public static Objects.Array Split(Value self, Value splitter)
{
string? _splitter = splitter.Ptr.As<Objects.String>()?.Value;
string? _string = self.Ptr.As<Objects.String>()?.Value;
if (_string is null)
return new Objects.Array([]);
if (_splitter is null)
return new Objects.Array([ self ]);
string[] parts = _string.Split(_splitter);
Value[] values = new Value[parts.Length];
for (int i = 0; i < parts.Length; i++)
values[i] = Objects.String.Make(parts[i]);
return new Objects.Array(values);
}
}
[ExtensionClass(ValueType = ValueType.Array)]
public static class ArrayExtensions
{
[ExtensionMethod]
public static long Length(Objects.Array self)
{
return self.Data.LongLength;
}
}