Skip to content

fix: handle InvocationExpression pattern for MemoryExtensions.Contains (.NET 10)#80

Merged
korchak-aleksandr merged 11 commits intomainfrom
korchak/fix-memory-extensions-invocation-expression
Mar 24, 2026
Merged

fix: handle InvocationExpression pattern for MemoryExtensions.Contains (.NET 10)#80
korchak-aleksandr merged 11 commits intomainfrom
korchak/fix-memory-extensions-invocation-expression

Conversation

@korchak-aleksandr
Copy link
Copy Markdown
Collaborator

@korchak-aleksandr korchak-aleksandr commented Mar 24, 2026

Problem

10.8.0 (PR #76) fixed MemoryExtensions.Contains for Pattern 1, but missed Pattern 2.

In .NET 10, array.Contains(value) in LINQ expression trees generates two patterns for the implicit T[]→ReadOnlySpan<T> conversion:

  1. MethodCallExpression(op_Implicit, [array]) — simple local capture ✅ fixed in 10.8.0
  2. InvocationExpression(ConstantExpression(pre_compiled_delegate), []) — closure compiled by DLR (e.g. after ExpandExpressions()) ❌ crashed

The VisitInvocation handler tried DynamicInvoke on the delegate, but ReadOnlySpan<T> is a ref struct — causing NotSupportedException.

Fix

Added FindArrayInClosure() — recursive search over the delegate's closure (depth ≤ 3, no heap allocation). Handles:

  • Direct T[] field (display class from regular C# closure)
  • Object[] Constants/Locals (runtime Closure from compiled expression lambdas), including nested delegates

TryExtractArrayFromSpanExpression() now delegates to FindArrayInClosure() for Pattern 2.

New test

ArrayContains_InvocationExpressionPattern_TranslatesToSqlIn — explicitly constructs Pattern 2 via Expression.Lambda(...).Compile(), was RED before this fix.

Test plan

  • dotnet build — 0 errors, 0 warnings
  • dotnet test63 passed, 17 skipped (net8.0 + net10.0)

…s in .NET 10

In .NET 10, array.Contains(value) in LINQ expression trees may generate two patterns:
1. MethodCallExpression(op_Implicit, [array]) — for simple local captures
2. InvocationExpression(ConstantExpression(pre_compiled_delegate), []) — when array
   is captured in a nested closure (e.g. passed as a method parameter)

The original 10.8.0 fix only handled pattern 1. This fix adds:
- TryExtractArrayFromSpanExpression() helper that handles both patterns
- For pattern 2: extracts the array from the delegate's closure target via reflection
- Broader condition check: mc.Arguments[0].Type is ReadOnlySpan<T> (not just MethodCallExpression)

New test ArrayContains_PassedAsParameter_TranslatesToSqlIn reproduces pattern 2.
The compiled delegate from Expression.Lambda(...).Compile() may have a
two-level closure: outer Closure.Constants contains another compiled delegate
whose Closure.Constants contains the actual array. Added FindArrayInClosure()
that recursively searches delegate closures (up to depth 3) to handle all
nesting patterns produced by the DLR expression compiler.

Also updated ArrayContains_InvocationExpressionPattern_TranslatesToSqlIn test
to directly construct the InvocationExpression using Expression.Lambda().Compile()
so it genuinely exercises Pattern 2 (RED without fix, GREEN with fix).
…s, remove unused test

- FindArrayInClosure: iterative BFS instead of recursion — no depth limit needed,
  works for any nesting produced by DLR; no risk of stack overflow
- TryExtractArrayFromSpanExpression: added braces on all return paths
- Removed ArrayContains_PassedAsParameter_TranslatesToSqlIn — it did not
  reproduce Pattern 2 (was identical to ArrayContains_TranslatesToSqlIn)
@korchak-aleksandr korchak-aleksandr merged commit d70b980 into main Mar 24, 2026
1 check passed
@korchak-aleksandr korchak-aleksandr deleted the korchak/fix-memory-extensions-invocation-expression branch March 24, 2026 14:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants