Skip to content

RM-9585 Rewrite LINQ queries using the span version of Contains - #1454

Open
a-ctor wants to merge 2 commits into
developfrom
feature/RM-9585-memory-extensions-contains
Open

RM-9585 Rewrite LINQ queries using the span version of Contains#1454
a-ctor wants to merge 2 commits into
developfrom
feature/RM-9585-memory-extensions-contains

Conversation

@a-ctor

@a-ctor a-ctor commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@a-ctor
a-ctor requested a review from MichaelKetting August 28, 2026 10:18
@a-ctor a-ctor self-assigned this Aug 28, 2026
@a-ctor a-ctor changed the title Feature/rm 9585 memory extensions contains RM-9585 Rewrite LINQ queries using the span version of Contains Aug 28, 2026

@MichaelKetting MichaelKetting left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got some simplification and a possible re-design of where the code should go. I hven't tried it. Please test.

/// While the usage get rewritten, it is also necessary to disable re-linqs evaluation as
/// it tries to constant fold the expression otherwise, which causes runtime exceptions.
/// </remarks>
public class ByRefLikeAwareEvaluatableExpressionFilter : EvaluatableExpressionFilterBase

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please create a ticket in sqlbackend. I think we should move this logic there eventually.

/// While the usage get rewritten, it is also necessary to disable re-linqs evaluation as
/// it tries to constant fold the expression otherwise, which causes runtime exceptions.
/// </remarks>
public class ByRefLikeAwareEvaluatableExpressionFilter : EvaluatableExpressionFilterBase

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we get a unit test for this please?


var transformerRegistry = ExpressionTransformerRegistry.CreateDefault();
var processor = ExpressionTreeParser.CreateDefaultProcessor(transformerRegistry);
transformerRegistry.Register(new SpanContainsExpressionTransformer());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please extend LinqProviderComponentFactoryTest

/// Transforms <see cref="MemoryExtensions"/>.<see cref="MemoryExtensions.Contains"/> calls generated by the C# 14 compiler back to their
/// <see cref="Enumerable"/>.<see cref="M:System.Linq.Enumerable.Contains``1(System.Collections.Generic.IEnumerable{``0},``0)"/> equivalent.
/// </summary>
public class SpanContainsExpressionTransformer : IExpressionTransformer<MethodCallExpression>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should also go into Remotion.Linq (RMLNQ) eventually. please create a ticket

Comment on lines +40 to +44
s_memoryExtensionContainsMethod = FindContainsMethod(typeof(MemoryExtensions), typeof(ReadOnlySpan<>), false);
s_memoryExtensionContainsMethodWithEqualityComparer = FindContainsMethod(typeof(MemoryExtensions), typeof(ReadOnlySpan<>), true);

s_enumerableContainsMethod = FindContainsMethod(typeof(Enumerable), typeof(IEnumerable<>), false);
s_enumerableContainsMethodWithEqualityComparer = FindContainsMethod(typeof(Enumerable), typeof(IEnumerable<>), true);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer we use this setup. It uses the existing helpers, the code is shorter and it's easier to understand for me.

      ... = MemberInfoFromExpressionUtility.GetGenericMethodDefinition(() => Enumerable.Contains((IEnumerable<object>)null, (object)null));
      ... = MemberInfoFromExpressionUtility.GetGenericMethodDefinition(() => Enumerable.Contains((IEnumerable<object>)null, (object)null, (IEqualityComparer<object>)null));
      var genericParameter = Type.MakeGenericMethodParameter(0);
      //  MemoryExtensions.Contains(ReadOnlySpan<int>.Empty, 0));
      ... = Assertion.IsNotNull(
              typeof(MemoryExtensions).GetMethod(
              nameof(MemoryExtensions.Contains),
              new[] { typeof(ReadOnlySpan<>).MakeGenericType(genericParameter), genericParameter }),
              "MemoryExtensions.Contains(ReadOnlySpan<T>, T)");
      ... = Assertion.IsNotNull(
              typeof(MemoryExtensions).GetMethod(
              nameof(MemoryExtensions.Contains),
              new[] { typeof(ReadOnlySpan<>).MakeGenericType(genericParameter), genericParameter, typeof(IEqualityComparer<>).MakeGenericType(genericParameter) }),
              "MemoryExtensions.Contains(ReadOnlySpan<T>, T, IEqualityComparer<T>)");

Interestingly enough, we could write the MemoryExtension.Contains for the IEqualityComparer using the MemberInfoFromExpressionUtility and skip most to all of the casts, but I like the fact that we're precise. Could live without the casts but this is the easiest way to ensure correctness.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's likely that we only need this part of the logic and the ResultOperator-registration in the other file and no Transformer. Please check


// Pre-filter for the relevant MemoryExtensions.Contains overloads
if (expression.Method.DeclaringType != typeof(MemoryExtensions)
|| expression.Method.Name != "Contains"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
|| expression.Method.Name != "Contains"
|| expression.Method.Name != nameof(MemoryExtensions.Contains)

Comment on lines +103 to +139
// Pre-filter for the relevant MemoryExtensions.Contains overloads
if (expression.Method.DeclaringType != typeof(MemoryExtensions)
|| expression.Method.Name != "Contains"
|| !expression.Method.IsGenericMethod
|| !IsEnumerableContainsMethod(expression.Method.GetGenericMethodDefinition()))
{
return expression;
}

var itemType = expression.Method.GetGenericArguments()[0];
var readOnlySpanType = typeof(ReadOnlySpan<>).MakeGenericType(itemType);

// We need to check for and remove the implicit span cast that gets generated.
// User code might also reach this point so we don't YoloCast(tm)
var collectionArgument = expression.Arguments[0];
if (collectionArgument is MethodCallExpression { Method.Name: "op_Implicit" } implicitConversion
&& implicitConversion.Method.DeclaringType == readOnlySpanType)
{
// Contains has two relevant overloads, one with and one without equality comparer.
// While re-linq does not support the equality comparer overload, the C# compiler
// rewrites the overload to the equality comparer one and passes a null value.
// This break any Contains usage.
// As such, we rewrite null equality comparer invocations to use the overload
// without an equality comparer.
if (expression.Arguments is [_, _, not ConstantExpression { Value: null }])
{
return Expression.Call(
s_enumerableContainsMethodWithEqualityComparer.MakeGenericMethod(itemType),
implicitConversion.Arguments[0],
expression.Arguments[1],
expression.Arguments[2]);
}

return Expression.Call(
s_enumerableContainsMethod.MakeGenericMethod(itemType),
implicitConversion.Arguments[0],
expression.Arguments[1]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the performance implications of this code. from the StopWatch, I think you took measurements.

Comment on lines +87 to +92
private static bool IsEnumerableContainsMethod (MethodInfo methodInfo)
{
return methodInfo == s_memoryExtensionContainsMethod
|| methodInfo == s_memoryExtensionContainsMethodWithEqualityComparer;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the name is incorrect. It should be IsMemoryExtenstionsContainsMethod

Comment on lines 101 to 107
protected virtual MethodInfoBasedNodeTypeRegistry CreateCustomNodeTypeProvider ()
{
var customNodeTypeRegistry = new MethodInfoBasedNodeTypeRegistry();

customNodeTypeRegistry.Register(
new[] { MemberInfoFromExpressionUtility.GetMethod((DomainObjectCollection obj) => obj.ContainsObject(null!)) },
typeof(ContainsExpressionNode));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check, I think we can plop the logic in here and simply register the ContainsExpressioNode for the two overloads of MemoryExtensions.Contains. I haven't read through the entire Transform-logic. If my suggestion works, I don't have to.

In particular, I haven't parsed why we also check for the two Contains-overloads on Enumerable. If these two also need wireing up for ContainsExpressionNode because re-linq isn't doing it on it's own, that's okay.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, running the tests, I understand we got op_Implicit in play, too and this is causing issues. Wonderful.

@MichaelKetting
MichaelKetting force-pushed the feature/RM-9585-memory-extensions-contains branch from f0126e4 to 5b1974f Compare August 28, 2026 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants