Skip to content

Change trimming workaround for Linq.Expressions usage of custom operators #79016

@vitek-karas

Description

@vitek-karas

When we annotated the System.Linq.Expressions for trimming we ran into a hard to solve problem with custom operators. The library will access custom operator methods via reflection in various places without a good way to track this via annotations or other means.
One of the goals was to make it possible to trim any expression tree construction code generated by the compiler. Unfortunately the compiler uses the overloads which do not specify the custom operator method as a parameter and instead it relies on code in the Expression class to find the custom operator on the target type (via reflection). For example for expression -x the compiler will generate a call to Expression.Negate(Expression.Parameter("x")).

We discussed a possibility of changing the compiler with the compiler team. The outcome is that the compiler team doesn't want to make this change in the compiler (to call the overloads which take MethodInfo - which would look like Expression.Negate(Expression.Parameter("x"), methodInfo)).

In .NET 6 and 7 the trimmer implemented a workaround for this. If the app uses System.Linq.Expressions.Expression type it will preserve all custom operators on all preserved types (there are some additional optimizations, but they're not interesting for this discussion). Due to this we were able to suppress the trim analysis warning generated in the library. See

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:UnrecognizedReflectionPattern",
Justification = "The trimmer doesn't remove operators when System.Linq.Expressions is used. See https://github.com/mono/linker/pull/2125.")]
private static UnaryExpression? GetUserDefinedUnaryOperator(ExpressionType unaryType, string name, Expression operand)

The proposal is to change this workaround to recognize the method Expression.GetUserDefinedUnaryOperatorOrThrow as intrinsic (or basically known method) for the trimmer. So instead of relying on presence of the Expression type, make it more specific and only preserve custom operators on preserved types when the method Expression.GetUserDefinedOperatorOrThrow is actually called by the application. In addition, all the calls to this method pass a constant string as the name of the operator method, so the trimmer could only preserve the custom operators which are actually potentially used (by collecting the constant strings from all callsites).

The advantages of this change:

  • More precise detection of the case where we need to preserve custom operators on types -> smaller apps which don't need the custom operators
  • More precise detection of custom operators which need to be preserved -> smaller apps even if they do need some custom operators
  • Potential for additional analysis improvements in the trimmer to eventually make the code fully analyzable and remove the warning suppression in the libraries (it's unclear what the complexity of that would be, but this change makes it possible).
  • There's also reduced complexity of the implementation of this behavior in the trimmer mainly because it has rich infrastructure for detection and handling of intrinsic method calls. The current implementation relying on type existence is "special"

The disadvantages:

  • Effectively making Expression.GetUserDefinedUnaryOperatorOrThrow a "semi-public" API - meaning we should not change it in any way (if we do we break tools, so possible but complex).

@MichalStrehovsky (the original author of this idea), @sbomer, @agocke, @eerhardt , @stephentoub

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions