refactor: compile where/sort/distinct and Advanced Search through expression trees (#2625)
## Summary `where`, `sort by`, `distinct` and the Advanced Search property test now compile through expression trees instead of the hand-rolled IL in `Emitter.cs`. The emitter and its three `Reflection.Emit` compilers were RunUO-era code from before expression trees existed; they were the only way to avoid per-object reflection at the time, and they are not any more. Net: ~1,900 lines of IL bookkeeping deleted, one comparison engine instead of two, faster per object, cheaper to compile, collectible, and `Nullable<T>` properties work. ## Why - **Bugs hid in the IL.** Equality on a type with value semantics but no `IComparable` (`TextDefinition`) was a raw `ceq`, so `where Message = 1060847` never matched. A chained binding (`Message.Number`) dereferenced every link unguarded, so the first swept object with a null intermediate killed the sweep with an NRE. A constant narrower or unsigned than `int` threw before compiling. A struct with no `CompareTo` reached `ceq` on two unboxed values, which is invalid IL. Every dynamic assembly was `Run`, so each `[global where` grew the process for good. - **Advanced Search had its own engine.** Per entity and per leaf it re-split the expression, scanned the runtime type's properties by name, read the value by reflection, re-parsed the right-hand side and dispatched on type through ~300 lines of `CompareValues`. Same job, second implementation, second set of bugs. ## What changed - `ICondition.Compile(MethodEmitter)` becomes `ICondition.Build(ParameterExpression)` returning an `Expression`. `ConditionalCompiler` assembles a `Func<object, bool>`, `SortCompiler` a `Comparison<T>`, `DistinctCompiler` both comparer interfaces over one lambda. The parsed constant is an `Expression.Constant`, so the generated type, its constructor and the per-condition field for non-primitive constants disappear with `PropertyValue`. - `PropertyExpressions` holds the shared pieces: the chain walk with its null-intermediate guard, `CompareTo` resolution with the old null ordering, the integral/enum operator path, and constant parsing. - `BaseExtension.Optimize` loses its `ref AssemblyEmitter` parameter. An out-of-tree extension that overrides `Optimize` needs to drop it. - Advanced Search keeps its grammar (`~` negates, `@` is AND, `|` is OR and binds looser, string `>` is "starts with") and translates each leaf into the same conditions, compiled once per declaring type per search and memoized across the workers. Float and double keep their typed-precision tolerance through a small `EpsilonCondition` in the Advanced Search folder. ## Semantics preserved - Equality on a non-comparable reference type is `object.Equals`, never reference identity. A non-comparable struct boxes into the same call. - A null intermediate in a chained binding is no match, and stays no match under negation. Sort and distinct read it as `default(T)`. - Unsigned relational compares stay unsigned; integral primitives and enums use the operator directly, nothing widens to a signed type. `float`, `double`, `decimal`, `string` and structs still go through the type's own `CompareTo`, so `string` equality stays culture-sensitive exactly as before. - Only `==` and `!=` are valid for non-comparable types; a relational operator still throws at build time. - `TypeCondition` is still first and still null-checks the cast target. ## Behavior changes - **`Nullable<T>` works**, with C# lifted semantics in `where`: two nulls are equal, a null and a value are unequal, a null satisfies no relation. Sort keeps a total order with unset values at one end. A null *reference* keeps the ordering it had. - **Advanced Search**: a leaf that cannot be parsed or resolved is no match even under `~` (it used to negate the failure and match every entity); `null` is the null value for equality on strings, nullables and reference types, as in `where`; dotted names walk into a property; static properties are no longer searchable. ## Measurements `where`, from the handoff (Debug test host, single condition, ratios not absolutes): | Approach | ns/object | Compile | Collectible | LOC | |---|---:|---:|---|---:| | `AssemblyBuilder` + IL (before) | 20.5 | 0.311 ms | no (`Run`) | ~1,916 | | Expression trees (after) | ~12 | 0.187 ms warm | yes | ~600 | Advanced Search, Release, one `SkillTeleporter`, 2M evaluations per leaf: | Leaf | Before | After | |---|---:|---:| | `Hue=5` | 137 ns | 20 ns | | `Name~~gate` | 121 ns | 39 ns | | `Skill=Magery` | 116 ns | 21 ns | | `Weight>0.5` | 122 ns | 28 ns | Plus 0.5 to 2 ms to compile each declaring type a search meets (12 ms for the first compile in the process). All of it runs on the search workers; nothing new touches the loop. ## Also fixed: Advanced Search map filters Found while testing the property test in game. The map boxes are independent checkboxes, but the worker applied each ticked map as "must be on this map", so ticking two or more (all maps and Internal, say) rejected every entity before any other filter ran. Present since #1649; the default of Felucca alone never showed it. An entity now passes when its map is any of the ticked ones, with none ticked meaning no map constraint. Pinned by a worker test. ## Test plan - [x] `UOContent.Tests`: 862 passed, 0 failed - [x] `Server.Tests`: 869 passed, 0 failed - [x] Every commit builds and its tests pass on its own (bisectable) - [ ] In game: `[global where`, `[area where`, `[condition`, `sort by`, `distinct`, Advanced Search property test
This commit is contained in:
parent
865a8cf218
commit
535a098996
24 changed files with 2294 additions and 2142 deletions
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
|
||||
namespace Server.Commands.Generic
|
||||
{
|
||||
|
|
@ -12,209 +11,17 @@ namespace Server.Commands.Generic
|
|||
|
||||
public interface ICondition
|
||||
{
|
||||
// Invoked during the constructor
|
||||
void Construct(TypeBuilder typeBuilder, ILGenerator il, int index);
|
||||
|
||||
// Target object will be loaded on the stack
|
||||
void Compile(MethodEmitter emitter);
|
||||
// `target` is the object under test, already cast to the conditional's type (so it is
|
||||
// null when the cast failed -- TypeCondition, always first, is what rejects that).
|
||||
Expression Build(ParameterExpression target);
|
||||
}
|
||||
|
||||
public sealed class TypeCondition : ICondition
|
||||
{
|
||||
public static TypeCondition Default = new();
|
||||
|
||||
void ICondition.Construct(TypeBuilder typeBuilder, ILGenerator il, int index)
|
||||
{
|
||||
}
|
||||
|
||||
void ICondition.Compile(MethodEmitter emitter)
|
||||
{
|
||||
// The object was safely cast to be the conditionals type
|
||||
// If it's null, then the type cast didn't work...
|
||||
|
||||
emitter.LoadNull();
|
||||
emitter.Compare(OpCodes.Ceq);
|
||||
emitter.LogicalNot();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PropertyValue
|
||||
{
|
||||
public PropertyValue(Type type, object value)
|
||||
{
|
||||
Type = type;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public Type Type { get; }
|
||||
|
||||
public object Value { get; private set; }
|
||||
|
||||
public FieldInfo Field { get; private set; }
|
||||
|
||||
public bool HasField => Field != null;
|
||||
|
||||
public void Load(MethodEmitter method)
|
||||
{
|
||||
if (Field != null)
|
||||
{
|
||||
method.LoadArgument(0);
|
||||
method.LoadField(Field);
|
||||
}
|
||||
else if (Value == null)
|
||||
{
|
||||
method.LoadNull(Type);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Value is int i)
|
||||
{
|
||||
method.Load(i);
|
||||
}
|
||||
else if (Value is long l)
|
||||
{
|
||||
method.Load(l);
|
||||
}
|
||||
else if (Value is float f)
|
||||
{
|
||||
method.Load(f);
|
||||
}
|
||||
else if (Value is double d)
|
||||
{
|
||||
method.Load(d);
|
||||
}
|
||||
else if (Value is char c)
|
||||
{
|
||||
method.Load(c);
|
||||
}
|
||||
else if (Value is bool b)
|
||||
{
|
||||
method.Load(b);
|
||||
}
|
||||
else if (Value is string s)
|
||||
{
|
||||
method.Load(s);
|
||||
}
|
||||
else if (Value is Enum e)
|
||||
{
|
||||
method.Load(e);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unrecognized comparison value.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Acquire(TypeBuilder typeBuilder, ILGenerator il, string fieldName)
|
||||
{
|
||||
if (Value is not string toParse)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Type.IsValueType && toParse == "null")
|
||||
{
|
||||
Value = null;
|
||||
}
|
||||
else if (Type == typeof(string))
|
||||
{
|
||||
if (toParse == @"@""null""")
|
||||
{
|
||||
toParse = "null";
|
||||
}
|
||||
|
||||
Value = toParse;
|
||||
}
|
||||
else if (Type.IsEnum)
|
||||
{
|
||||
Value = Enum.Parse(Type, toParse, true);
|
||||
}
|
||||
else if (Type == typeof(bool))
|
||||
{
|
||||
Value = bool.Parse(toParse);
|
||||
}
|
||||
else
|
||||
{
|
||||
MethodInfo parseMethod;
|
||||
object[] parseArgs;
|
||||
|
||||
var parseNumber = Type.GetMethod(
|
||||
"Parse",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
Types.ParseStringNumericParamTypes,
|
||||
null
|
||||
);
|
||||
|
||||
if (parseNumber != null)
|
||||
{
|
||||
var style = NumberStyles.Integer;
|
||||
|
||||
if (toParse.InsensitiveStartsWith("0x"))
|
||||
{
|
||||
style = NumberStyles.HexNumber;
|
||||
toParse = toParse[2..];
|
||||
}
|
||||
|
||||
parseMethod = parseNumber;
|
||||
parseArgs = new object[] { toParse, style };
|
||||
}
|
||||
else
|
||||
{
|
||||
var parseGeneral = Type.GetMethod(
|
||||
"Parse",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
Types.ParseStringParamTypes,
|
||||
null
|
||||
);
|
||||
|
||||
parseMethod = parseGeneral;
|
||||
parseArgs = new object[] { toParse, null };
|
||||
}
|
||||
|
||||
if (parseMethod != null)
|
||||
{
|
||||
Value = parseMethod.Invoke(null, parseArgs);
|
||||
|
||||
if (!Type.IsPrimitive)
|
||||
{
|
||||
Field = typeBuilder.DefineField(
|
||||
fieldName,
|
||||
Type,
|
||||
FieldAttributes.Private | FieldAttributes.InitOnly
|
||||
);
|
||||
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
|
||||
il.Emit(OpCodes.Ldstr, toParse);
|
||||
|
||||
if (parseArgs.Length == 2) // dirty evil hack :-(
|
||||
{
|
||||
if (parseArgs[1]?.GetType() == typeof(NumberStyles))
|
||||
{
|
||||
il.Emit(OpCodes.Ldc_I4, (int)parseArgs[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// IFormatProvider for `IParsable<T>.Parse()` method.
|
||||
il.Emit(OpCodes.Ldnull);
|
||||
}
|
||||
}
|
||||
|
||||
il.Emit(OpCodes.Call, parseMethod);
|
||||
il.Emit(OpCodes.Stfld, Field);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Unable to convert string \"{Value}\" into type '{Type}'."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expression ICondition.Build(ParameterExpression target) =>
|
||||
Expression.ReferenceNotEqual(target, Expression.Constant(null, target.Type));
|
||||
}
|
||||
|
||||
public abstract class PropertyCondition : ICondition
|
||||
|
|
@ -228,9 +35,22 @@ namespace Server.Commands.Generic
|
|||
m_Not = not;
|
||||
}
|
||||
|
||||
public abstract void Construct(TypeBuilder typeBuilder, ILGenerator il, int index);
|
||||
public abstract Expression Build(ParameterExpression target);
|
||||
|
||||
public abstract void Compile(MethodEmitter emitter);
|
||||
// A binding like Message.Number dereferences Message first, and Message is null on most
|
||||
// objects a sweep walks. That is "no match" rather than a crash -- and it stays "no match"
|
||||
// under negation, so the guard wraps the test after `not` has been applied to it.
|
||||
protected Expression Guarded(ParameterExpression target, Func<Expression, Expression> test) =>
|
||||
PropertyExpressions.Chain(
|
||||
target,
|
||||
m_Property,
|
||||
value =>
|
||||
{
|
||||
var result = test(value);
|
||||
return m_Not ? Expression.Not(result) : result;
|
||||
},
|
||||
Expression.Constant(false)
|
||||
);
|
||||
}
|
||||
|
||||
public enum StringOperator
|
||||
|
|
@ -247,125 +67,62 @@ namespace Server.Commands.Generic
|
|||
{
|
||||
private readonly bool m_IgnoreCase;
|
||||
private readonly StringOperator m_Operator;
|
||||
private readonly PropertyValue m_Value;
|
||||
private readonly object m_Value;
|
||||
|
||||
public StringCondition(Property property, bool not, StringOperator op, object value, bool ignoreCase)
|
||||
: base(property, not)
|
||||
{
|
||||
m_Operator = op;
|
||||
m_Value = new PropertyValue(property.Type, value);
|
||||
|
||||
m_Value = value;
|
||||
m_IgnoreCase = ignoreCase;
|
||||
}
|
||||
|
||||
public override void Construct(TypeBuilder typeBuilder, ILGenerator il, int index)
|
||||
public override Expression Build(ParameterExpression target)
|
||||
{
|
||||
m_Value.Acquire(typeBuilder, il, $"v{index}");
|
||||
}
|
||||
|
||||
public override void Compile(MethodEmitter emitter)
|
||||
{
|
||||
var inverse = false;
|
||||
|
||||
var type = m_IgnoreCase ? typeof(InsensitiveStringHelpers) : typeof(OrdinalStringHelpers);
|
||||
string methodName;
|
||||
|
||||
switch (m_Operator)
|
||||
if (m_Property.Type != typeof(string))
|
||||
{
|
||||
case StringOperator.NotEqual:
|
||||
{
|
||||
inverse = true;
|
||||
goto case StringOperator.Equal;
|
||||
}
|
||||
case StringOperator.Equal:
|
||||
{
|
||||
methodName = m_IgnoreCase ? "InsensitiveEquals" : "EqualsOrdinal";
|
||||
break;
|
||||
}
|
||||
|
||||
case StringOperator.Contains:
|
||||
{
|
||||
methodName = m_IgnoreCase ? "InsensitiveContains" : "ContainsOrdinal";
|
||||
break;
|
||||
}
|
||||
|
||||
case StringOperator.StartsWith:
|
||||
{
|
||||
methodName = m_IgnoreCase ? "InsensitiveStartsWith" : "StartsWithOrdinal";
|
||||
break;
|
||||
}
|
||||
|
||||
case StringOperator.EndsWith:
|
||||
{
|
||||
methodName = m_IgnoreCase ? "InsensitiveEndsWith" : "EndsWithOrdinal";
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new InvalidOperationException("Invalid string comparison operator.");
|
||||
}
|
||||
throw new InvalidOperationException("String operators require a string property.");
|
||||
}
|
||||
|
||||
if (m_Operator is StringOperator.Equal or StringOperator.NotEqual)
|
||||
var inverse = m_Operator == StringOperator.NotEqual;
|
||||
|
||||
var methodName = m_Operator switch
|
||||
{
|
||||
emitter.BeginCall(
|
||||
type.GetMethod(
|
||||
methodName,
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
[typeof(string), typeof(string)],
|
||||
null
|
||||
)
|
||||
);
|
||||
StringOperator.Equal or StringOperator.NotEqual => m_IgnoreCase ? "InsensitiveEquals" : "EqualsOrdinal",
|
||||
StringOperator.Contains => m_IgnoreCase ? "InsensitiveContains" : "ContainsOrdinal",
|
||||
StringOperator.StartsWith => m_IgnoreCase ? "InsensitiveStartsWith" : "StartsWithOrdinal",
|
||||
StringOperator.EndsWith => m_IgnoreCase ? "InsensitiveEndsWith" : "EndsWithOrdinal",
|
||||
_ => throw new InvalidOperationException("Invalid string comparison operator.")
|
||||
};
|
||||
|
||||
emitter.Chain(m_Property);
|
||||
m_Value.Load(emitter);
|
||||
var helper = (m_IgnoreCase ? typeof(InsensitiveStringHelpers) : typeof(OrdinalStringHelpers)).GetMethod(
|
||||
methodName,
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
[typeof(string), typeof(string)],
|
||||
null
|
||||
);
|
||||
|
||||
emitter.FinishCall();
|
||||
}
|
||||
else
|
||||
{
|
||||
var notNull = emitter.CreateLabel();
|
||||
var moveOn = emitter.CreateLabel();
|
||||
var constant = PropertyExpressions.Constant(typeof(string), m_Value);
|
||||
|
||||
var temp = emitter.AcquireTemp(m_Property.Type);
|
||||
return Guarded(
|
||||
target,
|
||||
value =>
|
||||
{
|
||||
Expression test = Expression.Call(helper, value, constant);
|
||||
|
||||
emitter.Chain(m_Property);
|
||||
// The equality helpers handle a null of their own; the rest need the guard.
|
||||
if (m_Operator is not (StringOperator.Equal or StringOperator.NotEqual))
|
||||
{
|
||||
test = Expression.AndAlso(
|
||||
Expression.ReferenceNotEqual(value, Expression.Constant(null, typeof(string))),
|
||||
test
|
||||
);
|
||||
}
|
||||
|
||||
emitter.StoreLocal(temp);
|
||||
emitter.LoadLocal(temp);
|
||||
|
||||
emitter.BranchIfTrue(notNull);
|
||||
|
||||
emitter.Load(false);
|
||||
emitter.Pop();
|
||||
emitter.Branch(moveOn);
|
||||
|
||||
emitter.MarkLabel(notNull);
|
||||
emitter.LoadLocal(temp);
|
||||
|
||||
emitter.BeginCall(
|
||||
type.GetMethod(
|
||||
methodName,
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
[typeof(string), typeof(string)],
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
m_Value.Load(emitter);
|
||||
|
||||
emitter.FinishCall();
|
||||
|
||||
emitter.MarkLabel(moveOn);
|
||||
}
|
||||
|
||||
if (m_Not != inverse)
|
||||
{
|
||||
emitter.LogicalNot();
|
||||
}
|
||||
return inverse ? Expression.Not(test) : test;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -382,219 +139,110 @@ namespace Server.Commands.Generic
|
|||
public sealed class ComparisonCondition : PropertyCondition
|
||||
{
|
||||
private readonly ComparisonOperator m_Operator;
|
||||
private readonly PropertyValue m_Value;
|
||||
private readonly object m_Value;
|
||||
|
||||
public ComparisonCondition(Property property, bool not, ComparisonOperator op, object value)
|
||||
: base(property, not)
|
||||
{
|
||||
m_Operator = op;
|
||||
m_Value = new PropertyValue(property.Type, value);
|
||||
m_Value = value;
|
||||
}
|
||||
|
||||
public override void Construct(TypeBuilder typeBuilder, ILGenerator il, int index)
|
||||
public override Expression Build(ParameterExpression target)
|
||||
{
|
||||
m_Value.Acquire(typeBuilder, il, $"v{index}");
|
||||
}
|
||||
var constant = PropertyExpressions.Constant(m_Property.Type, m_Value);
|
||||
|
||||
public override void Compile(MethodEmitter emitter)
|
||||
{
|
||||
emitter.Chain(m_Property);
|
||||
|
||||
var inverse = false;
|
||||
|
||||
var couldCompare =
|
||||
emitter.CompareTo(1, () => { m_Value.Load(emitter); });
|
||||
|
||||
if (couldCompare)
|
||||
{
|
||||
emitter.Load(0);
|
||||
|
||||
switch (m_Operator)
|
||||
return Guarded(
|
||||
target,
|
||||
value =>
|
||||
{
|
||||
case ComparisonOperator.Equal:
|
||||
{
|
||||
emitter.Compare(OpCodes.Ceq);
|
||||
break;
|
||||
}
|
||||
if (PropertyExpressions.TryRelational(value, constant, m_Operator, out var test))
|
||||
{
|
||||
return test;
|
||||
}
|
||||
|
||||
case ComparisonOperator.NotEqual:
|
||||
{
|
||||
emitter.Compare(OpCodes.Ceq);
|
||||
inverse = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case ComparisonOperator.Greater:
|
||||
{
|
||||
emitter.Compare(OpCodes.Cgt);
|
||||
break;
|
||||
}
|
||||
|
||||
case ComparisonOperator.GreaterEqual:
|
||||
{
|
||||
emitter.Compare(OpCodes.Clt);
|
||||
inverse = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case ComparisonOperator.Lesser:
|
||||
{
|
||||
emitter.Compare(OpCodes.Clt);
|
||||
break;
|
||||
}
|
||||
|
||||
case ComparisonOperator.LesserEqual:
|
||||
{
|
||||
emitter.Compare(OpCodes.Cgt);
|
||||
inverse = true;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new InvalidOperationException("Invalid comparison operator.");
|
||||
}
|
||||
// This type is -not- comparable. We can only support == and != operations.
|
||||
return m_Operator switch
|
||||
{
|
||||
ComparisonOperator.Equal => PropertyExpressions.ValueEquals(value, constant),
|
||||
ComparisonOperator.NotEqual => Expression.Not(PropertyExpressions.ValueEquals(value, constant)),
|
||||
ComparisonOperator.Greater or ComparisonOperator.GreaterEqual
|
||||
or ComparisonOperator.Lesser or ComparisonOperator.LesserEqual =>
|
||||
throw new InvalidOperationException("Property does not support relational comparisons."),
|
||||
_ => throw new InvalidOperationException("Invalid operator.")
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// This type is -not- comparable
|
||||
// We can only support == and != operations
|
||||
|
||||
m_Value.Load(emitter);
|
||||
|
||||
switch (m_Operator)
|
||||
{
|
||||
case ComparisonOperator.Equal:
|
||||
{
|
||||
emitter.Compare(OpCodes.Ceq);
|
||||
break;
|
||||
}
|
||||
|
||||
case ComparisonOperator.NotEqual:
|
||||
{
|
||||
emitter.Compare(OpCodes.Ceq);
|
||||
inverse = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case ComparisonOperator.Greater:
|
||||
case ComparisonOperator.GreaterEqual:
|
||||
case ComparisonOperator.Lesser:
|
||||
case ComparisonOperator.LesserEqual:
|
||||
{
|
||||
throw new InvalidOperationException("Property does not support relational comparisons.");
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new InvalidOperationException("Invalid operator.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_Not != inverse)
|
||||
{
|
||||
emitter.LogicalNot();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConditionalCompiler
|
||||
{
|
||||
public static IConditional Compile(AssemblyEmitter assembly, Type objectType, ICondition[] conditions, int index)
|
||||
private sealed class CompiledConditional : IConditional
|
||||
{
|
||||
var typeBuilder = assembly.DefineType(
|
||||
$"__conditional{index}",
|
||||
TypeAttributes.Public,
|
||||
typeof(object)
|
||||
);
|
||||
{
|
||||
var ctor = typeBuilder.DefineConstructor(
|
||||
MethodAttributes.Public,
|
||||
CallingConventions.Standard,
|
||||
Type.EmptyTypes
|
||||
);
|
||||
private readonly Func<object, bool> _verify;
|
||||
|
||||
var il = ctor.GetILGenerator();
|
||||
public CompiledConditional(Func<object, bool> verify) => _verify = verify;
|
||||
|
||||
// : base()
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
|
||||
public bool Verify(object obj) => _verify(obj);
|
||||
}
|
||||
|
||||
for (var i = 0; i < conditions.Length; ++i)
|
||||
/// <summary>
|
||||
/// Compiles a conjunction of conditions over <paramref name="objectType" /> into a single
|
||||
/// delegate. The conditions short-circuit left to right, so <see cref="TypeCondition" />
|
||||
/// comes first and the rest can assume a non-null, correctly typed target.
|
||||
/// </summary>
|
||||
public static IConditional Compile(Type objectType, ICondition[] conditions) =>
|
||||
new CompiledConditional(Build(objectType, conditions).Compile());
|
||||
|
||||
public static Expression<Func<object, bool>> Build(Type objectType, ICondition[] conditions) =>
|
||||
Lambda(objectType, target => Conjunction(target, conditions));
|
||||
|
||||
/// <summary>
|
||||
/// A disjunction of conjunctions -- <c>(a and b) or (c and d)</c> -- as one lambda, for
|
||||
/// callers that would otherwise compile every group separately and loop over them.
|
||||
/// </summary>
|
||||
public static Expression<Func<object, bool>> Build(Type objectType, ICondition[][] groups) =>
|
||||
Lambda(
|
||||
objectType,
|
||||
target =>
|
||||
{
|
||||
conditions[i].Construct(typeBuilder, il, i);
|
||||
}
|
||||
Expression body = groups.Length > 0 ? Conjunction(target, groups[0]) : Expression.Constant(false);
|
||||
|
||||
// return;
|
||||
il.Emit(OpCodes.Ret);
|
||||
}
|
||||
|
||||
typeBuilder.AddInterfaceImplementation(typeof(IConditional));
|
||||
|
||||
MethodBuilder compareMethod;
|
||||
{
|
||||
var emitter = new MethodEmitter(typeBuilder);
|
||||
|
||||
emitter.Define(
|
||||
/* name */ "Verify",
|
||||
/* attr */
|
||||
MethodAttributes.Public | MethodAttributes.Virtual,
|
||||
/* return */
|
||||
typeof(bool),
|
||||
/* params */
|
||||
new[] { typeof(object) }
|
||||
);
|
||||
|
||||
var obj = emitter.CreateLocal(objectType);
|
||||
var eq = emitter.CreateLocal(typeof(bool));
|
||||
|
||||
emitter.LoadArgument(1);
|
||||
emitter.CastAs(objectType);
|
||||
emitter.StoreLocal(obj);
|
||||
|
||||
var done = emitter.CreateLabel();
|
||||
|
||||
for (var i = 0; i < conditions.Length; ++i)
|
||||
{
|
||||
if (i > 0)
|
||||
for (var i = 1; i < groups.Length; ++i)
|
||||
{
|
||||
emitter.LoadLocal(eq);
|
||||
|
||||
emitter.BranchIfFalse(done);
|
||||
body = Expression.OrElse(body, Conjunction(target, groups[i]));
|
||||
}
|
||||
|
||||
emitter.LoadLocal(obj);
|
||||
|
||||
conditions[i].Compile(emitter);
|
||||
|
||||
emitter.StoreLocal(eq);
|
||||
return body;
|
||||
}
|
||||
);
|
||||
|
||||
emitter.MarkLabel(done);
|
||||
private static Expression Conjunction(ParameterExpression target, ICondition[] conditions)
|
||||
{
|
||||
Expression body = conditions.Length > 0 ? conditions[0].Build(target) : Expression.Constant(true);
|
||||
|
||||
emitter.LoadLocal(eq);
|
||||
|
||||
emitter.Return();
|
||||
|
||||
typeBuilder.DefineMethodOverride(
|
||||
emitter.Method,
|
||||
typeof(IConditional).GetMethod(
|
||||
"Verify",
|
||||
new[]
|
||||
{
|
||||
typeof(object)
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
compareMethod = emitter.Method;
|
||||
for (var i = 1; i < conditions.Length; ++i)
|
||||
{
|
||||
body = Expression.AndAlso(body, conditions[i].Build(target));
|
||||
}
|
||||
|
||||
var conditionalType = typeBuilder.CreateType();
|
||||
return body;
|
||||
}
|
||||
|
||||
return conditionalType.CreateInstance<IConditional>();
|
||||
private static Expression<Func<object, bool>> Lambda(Type objectType, Func<ParameterExpression, Expression> body)
|
||||
{
|
||||
var obj = Expression.Parameter(typeof(object), "obj");
|
||||
var target = Expression.Variable(objectType, "target");
|
||||
|
||||
return Expression.Lambda<Func<object, bool>>(
|
||||
Expression.Block(
|
||||
[target],
|
||||
Expression.Assign(target, Expression.TypeAs(obj, objectType)),
|
||||
body(target)
|
||||
),
|
||||
obj
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,252 +1,95 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Server.Commands.Generic
|
||||
{
|
||||
public static class DistinctCompiler
|
||||
{
|
||||
public static IComparer<T> Compile<T>(AssemblyEmitter assembly, Type objectType, Property[] props)
|
||||
private sealed class DistinctComparer<T> : IComparer<T>, IEqualityComparer<T>
|
||||
{
|
||||
var typeBuilder = assembly.DefineType(
|
||||
"__distinct",
|
||||
TypeAttributes.Public,
|
||||
typeof(object)
|
||||
private readonly Comparison<T> _compare;
|
||||
private readonly Func<T, int> _hash;
|
||||
|
||||
public DistinctComparer(Comparison<T> compare, Func<T, int> hash)
|
||||
{
|
||||
_compare = compare;
|
||||
_hash = hash;
|
||||
}
|
||||
|
||||
public int Compare(T x, T y) => _compare(x, y);
|
||||
|
||||
public bool Equals(T x, T y) => _compare(x, y) == 0;
|
||||
|
||||
public int GetHashCode(T obj) => _hash(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A comparer that treats two objects as the same when every one of
|
||||
/// <paramref name="props" /> reads equal on both. Ordering is the sort compiler's, all
|
||||
/// ascending, so the result doubles as an <see cref="IEqualityComparer{T}" />.
|
||||
/// </summary>
|
||||
public static IComparer<T> Compile<T>(Type objectType, Property[] props)
|
||||
{
|
||||
var signs = new int[props.Length];
|
||||
Array.Fill(signs, 1);
|
||||
|
||||
return new DistinctComparer<T>(
|
||||
SortCompiler.Build<T>(objectType, props, signs).Compile(),
|
||||
BuildHash<T>(objectType, props).Compile()
|
||||
);
|
||||
}
|
||||
|
||||
// XOR of each property's hash; a null reference hashes to 0 and an int hashes to itself.
|
||||
public static Expression<Func<T, int>> BuildHash<T>(Type objectType, Property[] props)
|
||||
{
|
||||
var arg = Expression.Parameter(typeof(T), "obj");
|
||||
var target = Expression.Variable(objectType, "target");
|
||||
|
||||
Expression hash = Expression.Constant(0);
|
||||
|
||||
for (var i = 0; i < props.Length; ++i)
|
||||
{
|
||||
var ctor = typeBuilder.DefineConstructor(
|
||||
MethodAttributes.Public,
|
||||
CallingConventions.Standard,
|
||||
Type.EmptyTypes
|
||||
);
|
||||
var part = HashOf(target, props[i]);
|
||||
|
||||
var il = ctor.GetILGenerator();
|
||||
|
||||
// : base()
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(
|
||||
OpCodes.Call,
|
||||
typeof(T).GetConstructor(Type.EmptyTypes) ??
|
||||
throw new Exception($"Could not find empty constructor for type {typeof(T).FullName}")
|
||||
);
|
||||
|
||||
// return;
|
||||
il.Emit(OpCodes.Ret);
|
||||
hash = i == 0 ? part : Expression.ExclusiveOr(hash, part);
|
||||
}
|
||||
|
||||
typeBuilder.AddInterfaceImplementation(typeof(IComparer<T>));
|
||||
return Expression.Lambda<Func<T, int>>(
|
||||
Expression.Block(
|
||||
[target],
|
||||
Expression.Assign(target, Expression.TypeAs(arg, objectType)),
|
||||
hash
|
||||
),
|
||||
arg
|
||||
);
|
||||
}
|
||||
|
||||
MethodBuilder compareMethod;
|
||||
private static Expression HashOf(Expression target, Property prop)
|
||||
{
|
||||
var read = PropertyExpressions.ChainOrDefault(target, prop);
|
||||
var type = prop.Type;
|
||||
|
||||
if (type == typeof(int))
|
||||
{
|
||||
var emitter = new MethodEmitter(typeBuilder);
|
||||
|
||||
emitter.Define(
|
||||
/* name */ "Compare",
|
||||
/* attr */
|
||||
MethodAttributes.Public | MethodAttributes.Virtual,
|
||||
/* return */
|
||||
typeof(int),
|
||||
/* params */
|
||||
new[] { typeof(T), typeof(T) }
|
||||
);
|
||||
|
||||
var a = emitter.CreateLocal(objectType);
|
||||
var b = emitter.CreateLocal(objectType);
|
||||
|
||||
var v = emitter.CreateLocal(typeof(int));
|
||||
|
||||
emitter.LoadArgument(1);
|
||||
emitter.CastAs(objectType);
|
||||
emitter.StoreLocal(a);
|
||||
|
||||
emitter.LoadArgument(2);
|
||||
emitter.CastAs(objectType);
|
||||
emitter.StoreLocal(b);
|
||||
|
||||
emitter.Load(0);
|
||||
emitter.StoreLocal(v);
|
||||
|
||||
var end = emitter.CreateLabel();
|
||||
|
||||
for (var i = 0; i < props.Length; ++i)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
emitter.LoadLocal(v);
|
||||
emitter.BranchIfTrue(end);
|
||||
}
|
||||
|
||||
var prop = props[i];
|
||||
|
||||
emitter.LoadLocal(a);
|
||||
emitter.Chain(prop);
|
||||
|
||||
var couldCompare =
|
||||
emitter.CompareTo(
|
||||
1,
|
||||
() =>
|
||||
{
|
||||
emitter.LoadLocal(b);
|
||||
emitter.Chain(prop);
|
||||
}
|
||||
);
|
||||
|
||||
if (!couldCompare)
|
||||
{
|
||||
throw new InvalidOperationException("Property is not comparable.");
|
||||
}
|
||||
|
||||
emitter.StoreLocal(v);
|
||||
}
|
||||
|
||||
emitter.MarkLabel(end);
|
||||
|
||||
emitter.LoadLocal(v);
|
||||
emitter.Return();
|
||||
|
||||
typeBuilder.DefineMethodOverride(
|
||||
emitter.Method,
|
||||
typeof(IComparer<T>).GetMethod(
|
||||
"Compare",
|
||||
new[]
|
||||
{
|
||||
typeof(T),
|
||||
typeof(T)
|
||||
}
|
||||
) ?? throw new Exception($"No Compare method found for type {typeof(T).FullName}")
|
||||
);
|
||||
|
||||
compareMethod = emitter.Method;
|
||||
return read;
|
||||
}
|
||||
|
||||
typeBuilder.AddInterfaceImplementation(typeof(IEqualityComparer<T>));
|
||||
var value = Expression.Variable(type, prop.Binding);
|
||||
var getHashCode = type.GetMethod("GetHashCode", Type.EmptyTypes) ?? typeof(object).GetMethod("GetHashCode", Type.EmptyTypes)!;
|
||||
|
||||
Expression hash = Expression.Call(value, getHashCode);
|
||||
|
||||
if (!type.IsValueType)
|
||||
{
|
||||
var emitter = new MethodEmitter(typeBuilder);
|
||||
|
||||
emitter.Define(
|
||||
/* name */ "Equals",
|
||||
/* attr */
|
||||
MethodAttributes.Public | MethodAttributes.Virtual,
|
||||
/* return */
|
||||
typeof(bool),
|
||||
/* params */
|
||||
new[] { typeof(T), typeof(T) }
|
||||
);
|
||||
|
||||
emitter.Generator.Emit(OpCodes.Ldarg_0);
|
||||
emitter.Generator.Emit(OpCodes.Ldarg_1);
|
||||
emitter.Generator.Emit(OpCodes.Ldarg_2);
|
||||
|
||||
emitter.Generator.Emit(OpCodes.Call, compareMethod);
|
||||
|
||||
emitter.Generator.Emit(OpCodes.Ldc_I4_0);
|
||||
|
||||
emitter.Generator.Emit(OpCodes.Ceq);
|
||||
|
||||
emitter.Generator.Emit(OpCodes.Ret);
|
||||
|
||||
typeBuilder.DefineMethodOverride(
|
||||
emitter.Method,
|
||||
typeof(IEqualityComparer<T>).GetMethod(
|
||||
"Equals",
|
||||
new[]
|
||||
{
|
||||
typeof(T),
|
||||
typeof(T)
|
||||
}
|
||||
) ?? throw new Exception($"No Equals method found for type {typeof(T).FullName}")
|
||||
hash = Expression.Condition(
|
||||
Expression.ReferenceNotEqual(value, Expression.Constant(null, type)),
|
||||
hash,
|
||||
Expression.Constant(0)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
var emitter = new MethodEmitter(typeBuilder);
|
||||
|
||||
emitter.Define(
|
||||
/* name */ "GetHashCode",
|
||||
/* attr */
|
||||
MethodAttributes.Public | MethodAttributes.Virtual,
|
||||
/* return */
|
||||
typeof(int),
|
||||
/* params */
|
||||
new[] { typeof(T) }
|
||||
);
|
||||
|
||||
var obj = emitter.CreateLocal(objectType);
|
||||
|
||||
emitter.LoadArgument(1);
|
||||
emitter.CastAs(objectType);
|
||||
emitter.StoreLocal(obj);
|
||||
|
||||
for (var i = 0; i < props.Length; ++i)
|
||||
{
|
||||
var prop = props[i];
|
||||
|
||||
emitter.LoadLocal(obj);
|
||||
emitter.Chain(prop);
|
||||
|
||||
var active = emitter.Active;
|
||||
|
||||
var getHashCode = active.GetMethod("GetHashCode", Type.EmptyTypes)
|
||||
?? typeof(T).GetMethod("GetHashCode", Type.EmptyTypes);
|
||||
|
||||
if (active != typeof(int))
|
||||
{
|
||||
if (!active.IsValueType)
|
||||
{
|
||||
var value = emitter.AcquireTemp(active);
|
||||
|
||||
var valueNotNull = emitter.CreateLabel();
|
||||
var done = emitter.CreateLabel();
|
||||
|
||||
emitter.StoreLocal(value);
|
||||
emitter.LoadLocal(value);
|
||||
|
||||
emitter.BranchIfTrue(valueNotNull);
|
||||
|
||||
emitter.Load(0);
|
||||
emitter.Pop(typeof(int));
|
||||
|
||||
emitter.Branch(done);
|
||||
|
||||
emitter.MarkLabel(valueNotNull);
|
||||
|
||||
emitter.LoadLocal(value);
|
||||
emitter.Call(getHashCode);
|
||||
|
||||
emitter.ReleaseTemp(value);
|
||||
|
||||
emitter.MarkLabel(done);
|
||||
}
|
||||
else
|
||||
{
|
||||
emitter.Call(getHashCode);
|
||||
}
|
||||
}
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
emitter.Xor();
|
||||
}
|
||||
}
|
||||
|
||||
emitter.Return();
|
||||
|
||||
typeBuilder.DefineMethodOverride(
|
||||
emitter.Method,
|
||||
typeof(IEqualityComparer<T>).GetMethod(
|
||||
"GetHashCode",
|
||||
new[]
|
||||
{
|
||||
typeof(T)
|
||||
}
|
||||
) ?? throw new Exception($"No GetHashCode method found for type {typeof(T).FullName}")
|
||||
);
|
||||
}
|
||||
|
||||
var comparerType = typeBuilder.CreateType();
|
||||
|
||||
return comparerType.CreateInstance<IComparer<T>>();
|
||||
return Expression.Block([value], Expression.Assign(value, read), hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,407 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Server.Commands.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Expression-tree fragments over a bound <see cref="Property" /> chain, shared by the
|
||||
/// conditional, sort and distinct compilers. Everything here builds an <see cref="Expression" />;
|
||||
/// the compilers assemble those into a lambda and hand <c>Compile()</c> the codegen.
|
||||
/// </summary>
|
||||
public static class PropertyExpressions
|
||||
{
|
||||
private static readonly MethodInfo _objectEquals = typeof(object).GetMethod(
|
||||
nameof(object.Equals),
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
[typeof(object), typeof(object)]
|
||||
)!;
|
||||
|
||||
/// <summary>
|
||||
/// Walks a property binding. A binding of more than one property (<c>Message.Number</c>)
|
||||
/// dereferences each link in turn, and any link but the last can legitimately be null -- an
|
||||
/// unset TextDefinition, an unparented item. Each reference-typed intermediate link is stored
|
||||
/// once and null-checked; a null there yields <paramref name="whenUnreadable" /> in place of
|
||||
/// whatever <paramref name="onValue" /> would have built from the final link.
|
||||
/// </summary>
|
||||
public static Expression Chain(
|
||||
Expression target,
|
||||
Property prop,
|
||||
Func<Expression, Expression> onValue,
|
||||
Expression whenUnreadable
|
||||
) => ChainFrom(target, prop.Chain, 0, onValue, whenUnreadable);
|
||||
|
||||
private static Expression ChainFrom(
|
||||
Expression current,
|
||||
PropertyInfo[] chain,
|
||||
int index,
|
||||
Func<Expression, Expression> onValue,
|
||||
Expression whenUnreadable
|
||||
)
|
||||
{
|
||||
var link = Expression.Property(current, chain[index]);
|
||||
|
||||
// The last link is the value being tested, so a null there is the caller's business.
|
||||
if (index == chain.Length - 1)
|
||||
{
|
||||
return onValue(link);
|
||||
}
|
||||
|
||||
if (link.Type.IsValueType)
|
||||
{
|
||||
return ChainFrom(link, chain, index + 1, onValue, whenUnreadable);
|
||||
}
|
||||
|
||||
var local = Expression.Variable(link.Type, chain[index].Name);
|
||||
|
||||
return Expression.Block(
|
||||
[local],
|
||||
Expression.Assign(local, link),
|
||||
Expression.Condition(
|
||||
Expression.ReferenceNotEqual(local, Expression.Constant(null, local.Type)),
|
||||
ChainFrom(local, chain, index + 1, onValue, whenUnreadable),
|
||||
whenUnreadable
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walks a binding for a caller that has no way to express "no match" -- ordering and
|
||||
/// grouping, where the value itself is the answer rather than a yes or no. An unreadable
|
||||
/// link yields <c>default(T)</c>, which is what a null link along the way amounts to; the
|
||||
/// comparers these feed already handle a null value.
|
||||
/// </summary>
|
||||
public static Expression ChainOrDefault(Expression target, Property prop) =>
|
||||
Chain(target, prop, static value => value, Expression.Default(prop.Type));
|
||||
|
||||
/// <summary>
|
||||
/// Equality for the "not comparable" path, which supports only == and !=. Reference equality
|
||||
/// would miss a type whose equality is by value -- <see cref="TextDefinition" /> among them --
|
||||
/// so this is static <c>object.Equals</c>, which honors the override and is null-safe on
|
||||
/// either side. Value types box; they only reach here when they have no <c>CompareTo</c>.
|
||||
/// </summary>
|
||||
public static Expression ValueEquals(Expression a, Expression b) =>
|
||||
Expression.Call(_objectEquals, Box(a), Box(b));
|
||||
|
||||
private static Expression Box(Expression e) =>
|
||||
e.Type == typeof(object) ? e : Expression.Convert(e, typeof(object));
|
||||
|
||||
/// <summary>
|
||||
/// A boolean test of <paramref name="a" /> against <paramref name="b" />. Integral primitives
|
||||
/// and enums compare with the operator itself -- on the unsigned types as unsigned; nothing
|
||||
/// here widens to a signed type. Everything else goes through <see cref="TryCompare" /> and
|
||||
/// tests the sign of the result. <c>Nullable<T></c> is lifted either way, as C# lifts it:
|
||||
/// two nulls are equal, a null and a value are unequal, and a null satisfies no relation.
|
||||
/// (A null <em>reference</em> keeps the ordering <see cref="TryCompare" /> gives it.) False
|
||||
/// when the type has no <c>CompareTo</c> at all, in which case only equality is meaningful.
|
||||
/// </summary>
|
||||
public static bool TryRelational(Expression a, Expression b, ComparisonOperator op, out Expression test)
|
||||
{
|
||||
var type = a.Type;
|
||||
var underlying = Nullable.GetUnderlyingType(type);
|
||||
var nonNullable = underlying ?? type;
|
||||
|
||||
if (underlying != null && !underlying.IsEnum && !IsIntegral(underlying))
|
||||
{
|
||||
return TryLiftedRelational(a, b, op, out test);
|
||||
}
|
||||
|
||||
if (nonNullable.IsEnum)
|
||||
{
|
||||
// Equal/NotEqual are defined on enums; the relational operators are not, so those
|
||||
// read the underlying integer. Convert lifts over Nullable<E> on its own.
|
||||
if (op is not (ComparisonOperator.Equal or ComparisonOperator.NotEqual))
|
||||
{
|
||||
var integer = Enum.GetUnderlyingType(nonNullable);
|
||||
|
||||
if (underlying != null)
|
||||
{
|
||||
integer = typeof(Nullable<>).MakeGenericType(integer);
|
||||
}
|
||||
|
||||
a = Expression.Convert(a, integer);
|
||||
b = Expression.Convert(b, integer);
|
||||
}
|
||||
|
||||
test = Relational(a, b, op);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsIntegral(nonNullable))
|
||||
{
|
||||
test = Relational(a, b, op);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryCompare(a, b, 1, out var comparison))
|
||||
{
|
||||
test = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
test = Relational(comparison, Expression.Constant(0), op);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Nullable<T> over a type that compares through CompareTo: the values compare when both are
|
||||
// present, and the HasValue flags decide otherwise, the way the language lifts an operator.
|
||||
private static bool TryLiftedRelational(Expression a, Expression b, ComparisonOperator op, out Expression test)
|
||||
{
|
||||
var left = Expression.Variable(a.Type, "left");
|
||||
var right = Expression.Variable(a.Type, "right");
|
||||
|
||||
var couldCompare = TryCompareValues(
|
||||
Expression.Property(left, "Value"),
|
||||
Expression.Property(right, "Value"),
|
||||
1,
|
||||
out var comparison
|
||||
);
|
||||
|
||||
if (!couldCompare)
|
||||
{
|
||||
test = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var leftHasValue = Expression.Property(left, "HasValue");
|
||||
var rightHasValue = Expression.Property(right, "HasValue");
|
||||
var both = Expression.AndAlso(leftHasValue, rightHasValue);
|
||||
var relation = Relational(comparison, Expression.Constant(0), op);
|
||||
|
||||
Expression lifted = op switch
|
||||
{
|
||||
ComparisonOperator.Equal => Expression.Condition(both, relation, Expression.Equal(leftHasValue, rightHasValue)),
|
||||
ComparisonOperator.NotEqual => Expression.Condition(both, relation, Expression.NotEqual(leftHasValue, rightHasValue)),
|
||||
_ => Expression.AndAlso(both, relation)
|
||||
};
|
||||
|
||||
test = Expression.Block(
|
||||
[left, right],
|
||||
Expression.Assign(left, a),
|
||||
Expression.Assign(right, b),
|
||||
lifted
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Expression Relational(Expression a, Expression b, ComparisonOperator op) =>
|
||||
op switch
|
||||
{
|
||||
ComparisonOperator.Equal => Expression.Equal(a, b),
|
||||
ComparisonOperator.NotEqual => Expression.NotEqual(a, b),
|
||||
ComparisonOperator.Greater => Expression.GreaterThan(a, b),
|
||||
ComparisonOperator.GreaterEqual => Expression.GreaterThanOrEqual(a, b),
|
||||
ComparisonOperator.Lesser => Expression.LessThan(a, b),
|
||||
ComparisonOperator.LesserEqual => Expression.LessThanOrEqual(a, b),
|
||||
_ => throw new InvalidOperationException("Invalid comparison operator.")
|
||||
};
|
||||
|
||||
private static bool IsIntegral(Type type) =>
|
||||
type == typeof(int) || type == typeof(long) || type == typeof(uint) || type == typeof(ulong)
|
||||
|| type == typeof(short) || type == typeof(ushort) || type == typeof(byte) || type == typeof(sbyte);
|
||||
|
||||
/// <summary>
|
||||
/// An <c>int</c>-valued comparison of <paramref name="a" /> against <paramref name="b" />
|
||||
/// with <c>CompareTo</c> semantics, multiplied by <paramref name="sign" />. A null on either
|
||||
/// side of a reference or nullable type is handled here rather than in the callee:
|
||||
/// <c>null.CompareTo(null) = 0</c>, <c>real.CompareTo(null) = -sign</c>,
|
||||
/// <c>null.CompareTo(real) = +sign</c>. False when the type has no <c>CompareTo</c>.
|
||||
/// </summary>
|
||||
public static bool TryCompare(Expression a, Expression b, int sign, out Expression comparison)
|
||||
{
|
||||
var type = a.Type;
|
||||
|
||||
// Both sides are read more than once below; pin them so a chained binding is walked once.
|
||||
var left = Expression.Variable(type, "left");
|
||||
var right = Expression.Variable(type, "right");
|
||||
|
||||
if (!TryCompareValues(left, right, sign, out var body))
|
||||
{
|
||||
comparison = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
comparison = Expression.Block(
|
||||
[left, right],
|
||||
Expression.Assign(left, a),
|
||||
Expression.Assign(right, b),
|
||||
body
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryCompareValues(Expression a, Expression b, int sign, out Expression comparison)
|
||||
{
|
||||
var type = a.Type;
|
||||
var underlying = Nullable.GetUnderlyingType(type);
|
||||
|
||||
if (underlying != null)
|
||||
{
|
||||
if (!TryCompareValues(Expression.Property(a, "Value"), Expression.Property(b, "Value"), sign, out var inner))
|
||||
{
|
||||
comparison = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
comparison = NullAware(Expression.Property(a, "HasValue"), Expression.Property(b, "HasValue"), inner, sign);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.IsEnum)
|
||||
{
|
||||
var integer = Enum.GetUnderlyingType(type);
|
||||
|
||||
return TryCompareValues(Expression.Convert(a, integer), Expression.Convert(b, integer), sign, out comparison);
|
||||
}
|
||||
|
||||
var compareTo = FindCompareTo(type);
|
||||
|
||||
if (compareTo == null)
|
||||
{
|
||||
comparison = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var parameterType = compareTo.GetParameters()[0].ParameterType;
|
||||
var argument = parameterType == type ? b : Expression.Convert(b, parameterType);
|
||||
|
||||
Expression call = Expression.Call(a, compareTo, argument);
|
||||
|
||||
if (sign == -1)
|
||||
{
|
||||
call = Expression.Negate(call);
|
||||
}
|
||||
|
||||
if (type.IsValueType)
|
||||
{
|
||||
comparison = call;
|
||||
return true;
|
||||
}
|
||||
|
||||
var nil = Expression.Constant(null, type);
|
||||
|
||||
comparison = NullAware(Expression.ReferenceNotEqual(a, nil), Expression.ReferenceNotEqual(b, nil), call, sign);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Expression NullAware(Expression aHasValue, Expression bHasValue, Expression compare, int sign) =>
|
||||
Expression.Condition(
|
||||
aHasValue,
|
||||
Expression.Condition(bHasValue, compare, Expression.Constant(-sign)),
|
||||
Expression.Condition(bHasValue, Expression.Constant(sign), Expression.Constant(0))
|
||||
);
|
||||
|
||||
private static MethodInfo FindCompareTo(Type type)
|
||||
{
|
||||
var compareTo = type.GetMethod("CompareTo", [type]);
|
||||
|
||||
if (compareTo != null)
|
||||
{
|
||||
return compareTo;
|
||||
}
|
||||
|
||||
/* There's a scenario where we might be trying to use CompareTo on an interface
|
||||
* which, while it doesn't explicitly implement CompareTo itself, is said to
|
||||
* extend IComparable indirectly. The implementation is implicitly passed off
|
||||
* to implementers, so the interface's own GetMethod("CompareTo") returns null.
|
||||
*/
|
||||
var ifaces = type.FindInterfaces(
|
||||
static (iface, _) => iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IComparable<>),
|
||||
null
|
||||
);
|
||||
|
||||
for (var i = 0; i < ifaces.Length; ++i)
|
||||
{
|
||||
if (ifaces[i].GetGenericArguments()[0].IsAssignableFrom(type))
|
||||
{
|
||||
return ifaces[i].GetMethod("CompareTo", [type]);
|
||||
}
|
||||
}
|
||||
|
||||
return typeof(IComparable).IsAssignableFrom(type)
|
||||
? typeof(IComparable).GetMethod("CompareTo", [typeof(object)])
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The right-hand side of a condition as a typed constant. A string is parsed the way the
|
||||
/// props gump would parse it: <c>null</c> for a reference or nullable type, <c>@"null"</c>
|
||||
/// for the literal string, names for enums, hex with a <c>0x</c> prefix for the numerics,
|
||||
/// and the type's own static <c>Parse</c> for everything else.
|
||||
/// </summary>
|
||||
public static ConstantExpression Constant(Type type, object value)
|
||||
{
|
||||
if (value is string text)
|
||||
{
|
||||
value = Parse(type, text);
|
||||
}
|
||||
|
||||
return Expression.Constant(value, type);
|
||||
}
|
||||
|
||||
private static object Parse(Type type, string text)
|
||||
{
|
||||
var underlying = Nullable.GetUnderlyingType(type);
|
||||
|
||||
if (text == "null" && (underlying != null || !type.IsValueType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var target = underlying ?? type;
|
||||
|
||||
if (target == typeof(string))
|
||||
{
|
||||
return text == @"@""null""" ? "null" : text;
|
||||
}
|
||||
|
||||
if (target.IsEnum)
|
||||
{
|
||||
return Enum.Parse(target, text, true);
|
||||
}
|
||||
|
||||
if (target == typeof(bool))
|
||||
{
|
||||
return bool.Parse(text);
|
||||
}
|
||||
|
||||
var parseNumber = target.GetMethod(
|
||||
"Parse",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
Types.ParseStringNumericParamTypes,
|
||||
null
|
||||
);
|
||||
|
||||
if (parseNumber != null)
|
||||
{
|
||||
var style = NumberStyles.Integer;
|
||||
|
||||
if (text.InsensitiveStartsWith("0x"))
|
||||
{
|
||||
style = NumberStyles.HexNumber;
|
||||
text = text[2..];
|
||||
}
|
||||
|
||||
return parseNumber.Invoke(null, [text, style]);
|
||||
}
|
||||
|
||||
var parseGeneral = target.GetMethod(
|
||||
"Parse",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
Types.ParseStringParamTypes,
|
||||
null
|
||||
);
|
||||
|
||||
if (parseGeneral != null)
|
||||
{
|
||||
return parseGeneral.Invoke(null, [text, null]);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to convert string \"{text}\" into type '{type}'.");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Server.Commands.Generic
|
||||
{
|
||||
|
|
@ -47,120 +46,83 @@ namespace Server.Commands.Generic
|
|||
|
||||
public static class SortCompiler
|
||||
{
|
||||
public static IComparer<T> Compile<T>(AssemblyEmitter assembly, Type objectType, OrderInfo[] orders)
|
||||
public static IComparer<T> Compile<T>(Type objectType, OrderInfo[] orders)
|
||||
{
|
||||
var typeBuilder = assembly.DefineType(
|
||||
"__sort",
|
||||
TypeAttributes.Public,
|
||||
typeof(T)
|
||||
var properties = new Property[orders.Length];
|
||||
var signs = new int[orders.Length];
|
||||
|
||||
for (var i = 0; i < orders.Length; ++i)
|
||||
{
|
||||
properties[i] = orders[i].Property;
|
||||
signs[i] = orders[i].Sign;
|
||||
}
|
||||
|
||||
return Comparer<T>.Create(Build<T>(objectType, properties, signs).Compile());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="Comparison{T}" /> over <paramref name="properties" />, taken in order: the
|
||||
/// first property that orders the two objects decides, each multiplied by its sign. Both
|
||||
/// arguments are cast to <paramref name="objectType" /> first; the bindings are read from
|
||||
/// that.
|
||||
/// </summary>
|
||||
public static Expression<Comparison<T>> Build<T>(Type objectType, Property[] properties, int[] signs)
|
||||
{
|
||||
var x = Expression.Parameter(typeof(T), "x");
|
||||
var y = Expression.Parameter(typeof(T), "y");
|
||||
|
||||
var a = Expression.Variable(objectType, "a");
|
||||
var b = Expression.Variable(objectType, "b");
|
||||
|
||||
return Expression.Lambda<Comparison<T>>(
|
||||
Expression.Block(
|
||||
[a, b],
|
||||
Expression.Assign(a, Expression.TypeAs(x, objectType)),
|
||||
Expression.Assign(b, Expression.TypeAs(y, objectType)),
|
||||
Ordered(a, b, properties, signs, 0)
|
||||
),
|
||||
x,
|
||||
y
|
||||
);
|
||||
}
|
||||
|
||||
private static Expression Ordered(Expression a, Expression b, Property[] properties, int[] signs, int index)
|
||||
{
|
||||
if (index >= properties.Length)
|
||||
{
|
||||
var ctor = typeBuilder.DefineConstructor(
|
||||
MethodAttributes.Public,
|
||||
CallingConventions.Standard,
|
||||
Type.EmptyTypes
|
||||
);
|
||||
|
||||
var il = ctor.GetILGenerator();
|
||||
|
||||
// : base()
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(
|
||||
OpCodes.Call,
|
||||
typeof(T).GetConstructor(Type.EmptyTypes) ??
|
||||
throw new Exception($"Could not find empty constructor for type {typeof(T).FullName}")
|
||||
);
|
||||
|
||||
// return;
|
||||
il.Emit(OpCodes.Ret);
|
||||
return Expression.Constant(0);
|
||||
}
|
||||
|
||||
typeBuilder.AddInterfaceImplementation(typeof(IComparer<T>));
|
||||
var prop = properties[index];
|
||||
|
||||
var couldCompare = PropertyExpressions.TryCompare(
|
||||
PropertyExpressions.ChainOrDefault(a, prop),
|
||||
PropertyExpressions.ChainOrDefault(b, prop),
|
||||
signs[index],
|
||||
out var comparison
|
||||
);
|
||||
|
||||
if (!couldCompare)
|
||||
{
|
||||
var emitter = new MethodEmitter(typeBuilder);
|
||||
|
||||
emitter.Define(
|
||||
/* name */ "Compare",
|
||||
/* attr */
|
||||
MethodAttributes.Public | MethodAttributes.Virtual,
|
||||
/* return */
|
||||
typeof(int),
|
||||
/* params */
|
||||
new[] { typeof(T), typeof(T) }
|
||||
);
|
||||
|
||||
var a = emitter.CreateLocal(objectType);
|
||||
var b = emitter.CreateLocal(objectType);
|
||||
|
||||
var v = emitter.CreateLocal(typeof(int));
|
||||
|
||||
emitter.LoadArgument(1);
|
||||
emitter.CastAs(objectType);
|
||||
emitter.StoreLocal(a);
|
||||
|
||||
emitter.LoadArgument(2);
|
||||
emitter.CastAs(objectType);
|
||||
emitter.StoreLocal(b);
|
||||
|
||||
emitter.Load(0);
|
||||
emitter.StoreLocal(v);
|
||||
|
||||
var end = emitter.CreateLabel();
|
||||
|
||||
for (var i = 0; i < orders.Length; ++i)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
emitter.LoadLocal(v);
|
||||
emitter.BranchIfTrue(end);
|
||||
}
|
||||
|
||||
var orderInfo = orders[i];
|
||||
|
||||
var prop = orderInfo.Property;
|
||||
var sign = orderInfo.Sign;
|
||||
|
||||
emitter.LoadLocal(a);
|
||||
emitter.Chain(prop);
|
||||
|
||||
var couldCompare =
|
||||
emitter.CompareTo(
|
||||
sign,
|
||||
() =>
|
||||
{
|
||||
emitter.LoadLocal(b);
|
||||
emitter.Chain(prop);
|
||||
}
|
||||
);
|
||||
|
||||
if (!couldCompare)
|
||||
{
|
||||
throw new InvalidOperationException("Property is not comparable.");
|
||||
}
|
||||
|
||||
emitter.StoreLocal(v);
|
||||
}
|
||||
|
||||
emitter.MarkLabel(end);
|
||||
|
||||
emitter.LoadLocal(v);
|
||||
emitter.Return();
|
||||
|
||||
typeBuilder.DefineMethodOverride(
|
||||
emitter.Method,
|
||||
typeof(IComparer<T>).GetMethod(
|
||||
"Compare",
|
||||
new[]
|
||||
{
|
||||
typeof(T),
|
||||
typeof(T)
|
||||
}
|
||||
) ?? throw new Exception($"No Compare method found for type {typeof(T).FullName}")
|
||||
);
|
||||
throw new InvalidOperationException("Property is not comparable.");
|
||||
}
|
||||
|
||||
var comparerType = typeBuilder.CreateType();
|
||||
return comparerType.CreateInstance<IComparer<T>>();
|
||||
if (index == properties.Length - 1)
|
||||
{
|
||||
return comparison;
|
||||
}
|
||||
|
||||
var v = Expression.Variable(typeof(int), "v");
|
||||
|
||||
return Expression.Block(
|
||||
[v],
|
||||
Expression.Assign(v, comparison),
|
||||
Expression.Condition(
|
||||
Expression.NotEqual(v, Expression.Constant(0)),
|
||||
v,
|
||||
Ordered(a, b, properties, signs, index + 1)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue