Browse Source

TransformExpressionTrees: document the ILAst patterns

Every converter now states the Expression.* call it matches and the ILAst it
produces, and the argument-count switches label the factory overload each case
stands for. The shapes were read off ILAst dumps of compiled expression trees
rather than from the factory signatures; two branches are documented as
unreachable, since no arithmetic or logical factory declares the four-argument
(left, right, liftToNull, method) overload their case matches.

Also drops the result-type local left in ConvertField, which BuildField
re-derives from the field and the type hint.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
pull/4091/head
Siegfried Pammer 2 weeks ago committed by Daniel Grunwald
parent
commit
154d60e2a3
  1. 417
      ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs

417
ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs

@ -39,6 +39,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms
{ {
/// <summary> /// <summary>
/// Returns true if the instruction matches the pattern for Expression.Lambda calls. /// Returns true if the instruction matches the pattern for Expression.Lambda calls.
///
/// call Lambda(&lt;body&gt;, &lt;parameter array&gt;)
///
/// where &lt;parameter array&gt; is either an empty parameter list (see
/// <see cref="IsEmptyParameterList"/>) or a Block of kind ArrayInitializer.
/// This is only a cheap pre-filter, the actual conversion is done by
/// <see cref="ConvertLambda"/>.
/// </summary> /// </summary>
static bool MightBeExpressionTree(ILInstruction inst, ILInstruction stmt) static bool MightBeExpressionTree(ILInstruction inst, ILInstruction stmt)
{ {
@ -53,6 +60,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return true; return true;
} }
/// <summary>
/// Matches the argument array of a call that has no arguments:
/// call System.Array.Empty(), newarr System.Linq.Expressions.ParameterExpression(...)
/// or newarr System.Linq.Expressions.Expression(...).
/// The array length is not inspected for the two newarr forms.
/// </summary>
static bool IsEmptyParameterList(ILInstruction inst) static bool IsEmptyParameterList(ILInstruction inst)
{ {
if (inst is CallInstruction emptyCall && emptyCall.Method.FullNameIs("System.Array", "Empty") && emptyCall.Arguments.Count == 0) if (inst is CallInstruction emptyCall && emptyCall.Method.FullNameIs("System.Array", "Empty") && emptyCall.Arguments.Count == 0)
@ -64,6 +77,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return false; return false;
} }
/// <summary>
/// stloc v(call Parameter(call GetTypeFromHandle(ldtypetoken T), ldstr "name"))
/// =&gt;
/// true, with parameterReferenceVar = v, type = T and name = "name".
///
/// v must be a single-definition local or stack slot of type
/// System.Linq.Expressions.ParameterExpression.
/// </summary>
bool MatchParameterVariableAssignment(ILInstruction expr, out ILVariable parameterReferenceVar, out IType type, out string name) bool MatchParameterVariableAssignment(ILInstruction expr, out ILVariable parameterReferenceVar, out IType type, out string name)
{ {
// stloc(v, call(Expression::Parameter, call(Type::GetTypeFromHandle, ldtoken(...)), ldstr(...))) // stloc(v, call(Expression::Parameter, call(Type::GetTypeFromHandle, ldtoken(...)), ldstr(...)))
@ -97,6 +118,15 @@ namespace ICSharpCode.Decompiler.IL.Transforms
CSharpConversions conversions; CSharpConversions conversions;
CSharpResolver resolver; CSharpResolver resolver;
/// <summary>
/// Starting at pos, collects the leading run of lambda parameter declarations
///
/// stloc v(call Parameter(call GetTypeFromHandle(ldtypetoken T), ldstr "name"))
///
/// then tries to convert the first statement that is not such a declaration; see
/// <see cref="TryConvertExpressionTree"/>. On success the parameter declarations
/// consumed by the converted tree are removed from the block.
/// </summary>
public void Run(Block block, int pos, StatementTransformContext context) public void Run(Block block, int pos, StatementTransformContext context)
{ {
if (!context.Settings.ExpressionTrees) if (!context.Settings.ExpressionTrees)
@ -125,6 +155,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
/// <summary>
/// Searches instruction for the first
///
/// call Lambda(&lt;body&gt;, &lt;parameter array&gt;)
///
/// and replaces it with the ILFunction built by <see cref="ConvertLambda"/>.
/// Nested control-flow blocks are not searched. Returns true if a tree was converted.
/// </summary>
bool TryConvertExpressionTree(ILInstruction instruction, ILInstruction statement) bool TryConvertExpressionTree(ILInstruction instruction, ILInstruction statement)
{ {
if (MightBeExpressionTree(instruction, statement)) if (MightBeExpressionTree(instruction, statement))
@ -154,6 +192,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms
/// <summary> /// <summary>
/// Converts a Expression.Lambda call into an ILFunction. /// Converts a Expression.Lambda call into an ILFunction.
/// If the conversion fails, null is returned. /// If the conversion fails, null is returned.
///
/// call Lambda(&lt;body&gt;, Block (ArrayInitializer) { stobj System.Object(delayex.ldelema System.Object(ldloc S, ldc.i4 0), ldloc V_0), ... })
/// =&gt;
/// ILFunction(&lt;parameters&gt;) { BlockContainer { Block { leave (&lt;converted body&gt;) } } }
///
/// The parameters are read from the array initializer by <see cref="ReadParameters"/>.
/// The call must return Expression&lt;TDelegate&gt;; the ILFunction gets
/// DelegateType = TDelegate and kind ExpressionTree if TDelegate is itself an
/// expression tree type, Delegate otherwise. The returned delegate does the actual
/// building: nothing is mutated until it is invoked.
/// </summary> /// </summary>
Func<ILInstruction> ConvertLambda(CallInstruction instruction) Func<ILInstruction> ConvertLambda(CallInstruction instruction)
{ {
@ -202,6 +250,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
/// <summary>
/// call Quote(&lt;lambda&gt;)
/// =&gt;
/// &lt;converted lambda&gt;
///
/// An argument that is already an ILFunction is passed through unchanged. Otherwise
/// the argument (typically a nested call Lambda(...)) is converted, and if that
/// yields an ILFunction its DelegateType and kind are taken from the return type of
/// the argument call; see <see cref="SetExpressionTreeFlag"/>.
/// </summary>
Func<ILInstruction> ConvertQuote(CallInstruction invocation) Func<ILInstruction> ConvertQuote(CallInstruction invocation)
{ {
if (invocation.Arguments.Count != 1) if (invocation.Arguments.Count != 1)
@ -231,12 +289,30 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
/// <summary>
/// Sets DelegateType and Kind of lambda from the return type of call: a return type
/// Expression&lt;TDelegate&gt; gives ILFunctionKind.ExpressionTree, any other type gives
/// ILFunctionKind.Delegate.
/// </summary>
void SetExpressionTreeFlag(ILFunction lambda, CallInstruction call) void SetExpressionTreeFlag(ILFunction lambda, CallInstruction call)
{ {
lambda.Kind = IsExpressionTree(call.Method.ReturnType) ? ILFunctionKind.ExpressionTree : ILFunctionKind.Delegate; lambda.Kind = IsExpressionTree(call.Method.ReturnType) ? ILFunctionKind.ExpressionTree : ILFunctionKind.Delegate;
lambda.DelegateType = call.Method.ReturnType; lambda.DelegateType = call.Method.ReturnType;
} }
/// <summary>
/// Reads the lambda parameter list from the ParameterExpression[] argument of a
/// call Lambda(...).
///
/// Block (ArrayInitializer) { stobj System.Object(delayex.ldelema System.Object(ldloc S, ldc.i4 i), ldloc V_i), ... }
/// =&gt;
/// one IParameter and one ILVariable of kind Parameter per element, using the type
/// and name recorded for V_i by <see cref="MatchParameterVariableAssignment"/>.
/// An empty parameter list (see <see cref="IsEmptyParameterList"/>) yields none.
///
/// Each ParameterExpression variable enters the mapping only once; its defining
/// stloc is queued for removal.
/// </summary>
bool ReadParameters(ILInstruction initializer, IList<IParameter> parameters, IList<ILVariable> parameterVariables, ITypeResolveContext resolveContext) bool ReadParameters(ILInstruction initializer, IList<IParameter> parameters, IList<ILVariable> parameterVariables, ITypeResolveContext resolveContext)
{ {
switch (initializer) switch (initializer)
@ -270,6 +346,21 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
/// <summary>
/// Converts one node of the expression tree into a Func&lt;ILInstruction&gt; building the
/// equivalent ILAst, or null if the node cannot be converted:
///
/// call &lt;name&gt;(...) on System.Linq.Expressions.Expression =&gt; the result of the
/// Convert* method for &lt;name&gt;, e.g. call Add(a, b) =&gt; binary.numeric.add(a, b).
/// ILFunction (an already converted nested lambda) =&gt; the same function, with an
/// expression tree DelegateType unwrapped to TDelegate and kind set to Delegate.
/// ldloc v, v a ParameterExpression =&gt; ldloc/ldloca of the mapped parameter variable,
/// or, for a not yet mapped parameter of an enclosing lambda,
/// expression.tree.cast T(ldloc v), so conversion can continue.
///
/// If typeHint is given and the built instruction has a different stack type, it is
/// wrapped in a conv to that stack type.
/// </summary>
Func<ILInstruction> ConvertInstruction(ILInstruction instruction, IType typeHint = null) Func<ILInstruction> ConvertInstruction(ILInstruction instruction, IType typeHint = null)
{ {
var inst = Convert(); var inst = Convert();
@ -437,10 +528,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
/// <summary>
/// Returns true for System.Linq.Expressions.Expression&lt;T&gt;.
/// </summary>
bool IsExpressionTree(IType delegateType) => delegateType is ParameterizedType pt bool IsExpressionTree(IType delegateType) => delegateType is ParameterizedType pt
&& pt.FullName == "System.Linq.Expressions.Expression" && pt.FullName == "System.Linq.Expressions.Expression"
&& pt.TypeArguments.Count == 1; && pt.TypeArguments.Count == 1;
/// <summary>
/// Returns T for System.Linq.Expressions.Expression&lt;T&gt;; any other type is returned unchanged.
/// </summary>
IType UnwrapExpressionTree(IType delegateType) IType UnwrapExpressionTree(IType delegateType)
{ {
if (delegateType is ParameterizedType pt && pt.FullName == "System.Linq.Expressions.Expression" && pt.TypeArguments.Count == 1) if (delegateType is ParameterizedType pt && pt.FullName == "System.Linq.Expressions.Expression" && pt.TypeArguments.Count == 1)
@ -450,6 +547,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return delegateType; return delegateType;
} }
/// <summary>
/// call ArrayIndex(array, index)
/// call ArrayIndex(array, argumentList) // multi-dimensional arrays
/// =&gt;
/// ldobj T(delayex.ldelema T(array, indices))
/// The element type T is taken from the inferred type of the converted array expression;
/// conversion fails if that type is not an array type.
/// </summary>
Func<ILInstruction> ConvertArrayIndex(CallInstruction invocation) Func<ILInstruction> ConvertArrayIndex(CallInstruction invocation)
{ {
if (invocation.Arguments.Count != 2) if (invocation.Arguments.Count != 2)
@ -480,6 +585,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return Convert; return Convert;
} }
/// <summary>
/// call ArrayLength(array)
/// =&gt;
/// ldlen.i4(array)
/// </summary>
Func<ILInstruction> ConvertArrayLength(CallInstruction invocation) Func<ILInstruction> ConvertArrayLength(CallInstruction invocation)
{ {
if (invocation.Arguments.Count != 1) if (invocation.Arguments.Count != 1)
@ -490,6 +600,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return () => new LdLen(StackType.I4, converted()); return () => new LdLen(StackType.I4, converted());
} }
/// <summary>
/// call Add(left, right) // built-in operator
/// call Add(left, right, MethodInfo) // user-defined operator
/// call Add(left, right, ldc.i4 isLiftedToNull, MethodInfo) // user-defined operator
/// =&gt;
/// binary.add.i4(left, right) | call op_Addition(left, right)
/// The two-argument shape infers both operand types: decimal operands select the operator
/// method named operatorName, everything else produces a BinaryNumericInstruction, lifted
/// if either operand type is nullable. Shift operators require an Int32 right operand, all
/// other operators require the two operand types to match. The four-argument shape lifts
/// the given method if the left operand type is nullable.
/// </summary>
Func<ILInstruction> ConvertBinaryNumericOperator(CallInstruction invocation, BinaryNumericOperator op, string operatorName, bool? isChecked = null) Func<ILInstruction> ConvertBinaryNumericOperator(CallInstruction invocation, BinaryNumericOperator op, string operatorName, bool? isChecked = null)
{ {
if (invocation.Arguments.Count < 2) if (invocation.Arguments.Count < 2)
@ -504,6 +626,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
IMember method; IMember method;
switch (invocation.Arguments.Count) switch (invocation.Arguments.Count)
{ {
// call Add(left, right): built-in operator, or the operator method of decimal
case 2: case 2:
return () => { return () => {
var leftInst = left(); var leftInst = left();
@ -538,12 +661,15 @@ namespace ICSharpCode.Decompiler.IL.Transforms
leftType.GetSign(), leftType.GetSign(),
isLifted: NullableType.IsNullable(leftType) || NullableType.IsNullable(rightType)); isLifted: NullableType.IsNullable(leftType) || NullableType.IsNullable(rightType));
}; };
// call Add(left, right, methodInfo): user-defined operator
case 3: case 3:
if (!MatchGetMethodFromHandle(invocation.Arguments[2], out method)) if (!MatchGetMethodFromHandle(invocation.Arguments[2], out method))
return null; return null;
return () => new Call((IMethod)method) { return () => new Call((IMethod)method) {
Arguments = { left(), right() } Arguments = { left(), right() }
}; };
// call Add(left, right, ldc.i4 liftToNull, methodInfo): the shape of the
// comparison factories; no arithmetic or bitwise factory declares it
case 4: case 4:
if (!invocation.Arguments[2].MatchLdcI4(out _)) if (!invocation.Arguments[2].MatchLdcI4(out _))
return null; return null;
@ -566,6 +692,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
/// <summary>
/// call Bind(castclass System.Reflection.MethodInfo(call GetMethodFromHandle(ldmembertoken set_P)), value)
/// call Bind(call GetFieldFromHandle(ldmembertoken F), value)
/// =&gt;
/// callvirt set_P(ldloc target, value)
/// stobj T(delayex.ldflda F(ldloc target), value)
/// The returned builder takes the variable holding the object being initialized.
/// </summary>
Func<ILVariable, ILInstruction> ConvertBind(CallInstruction invocation) Func<ILVariable, ILInstruction> ConvertBind(CallInstruction invocation)
{ {
if (invocation.Arguments.Count != 2) if (invocation.Arguments.Count != 2)
@ -605,6 +739,20 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return null; return null;
} }
/// <summary>
/// call Call(MethodInfo, argumentList) // static method
/// call Call(target, MethodInfo, argumentList) // target is ldnull for static methods
/// =&gt;
/// call M(arguments) | callvirt M(target, arguments)
///
/// Method group conversion:
/// call Call(call Constant(MethodInfo M, ...), MethodInfo MethodInfo.CreateDelegate, argumentList { call Constant(typeof(D), ...), targetObject })
/// =&gt;
/// newobj D..ctor(targetObject, ldftn M)
///
/// The argument list is normally a single array-initializer block; if it is not, the
/// remaining arguments of the invocation are taken as the argument list directly.
/// </summary>
Func<ILInstruction> ConvertCall(CallInstruction invocation) Func<ILInstruction> ConvertCall(CallInstruction invocation)
{ {
if (invocation.Arguments.Count < 2) if (invocation.Arguments.Count < 2)
@ -673,6 +821,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return BuildCall; return BuildCall;
} }
/// <summary>
/// Adapts a converted call target to the 'this' argument expected by a call on
/// expectedType: takes its address (ldloca or addressof) where a by-reference 'this' is
/// required, and boxes it where a boxed value type is required. If exactly one of the
/// expected type and the result is unknown, a conv to the other side's primitive type is
/// inserted, so that missing references do not produce mismatched call arguments.
/// </summary>
ILInstruction PrepareCallTarget(IType expectedType, ILInstruction target, IType targetType) ILInstruction PrepareCallTarget(IType expectedType, ILInstruction target, IType targetType)
{ {
ILInstruction result; ILInstruction result;
@ -718,6 +873,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return result; return result;
} }
/// <summary>
/// Returns the value of call Constant(value, typeToken); any other instruction is returned unchanged.
/// </summary>
ILInstruction UnpackConstant(ILInstruction inst) ILInstruction UnpackConstant(ILInstruction inst)
{ {
if (!(inst is CallInstruction call && call.Method.FullName == "System.Linq.Expressions.Expression.Constant" && call.Arguments.Count == 2)) if (!(inst is CallInstruction call && call.Method.FullName == "System.Linq.Expressions.Expression.Constant" && call.Arguments.Count == 2))
@ -725,6 +883,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return call.Arguments[0]; return call.Arguments[0];
} }
/// <summary>
/// Converts each argument using the corresponding parameter type of method as type hint.
/// Returns null if any argument cannot be converted.
/// </summary>
Func<ILInstruction>[] ConvertCallArguments(IList<ILInstruction> arguments, IMethod method) Func<ILInstruction>[] ConvertCallArguments(IList<ILInstruction> arguments, IMethod method)
{ {
var converted = new Func<ILInstruction>[arguments.Count]; var converted = new Func<ILInstruction>[arguments.Count];
@ -740,6 +902,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return converted; return converted;
} }
/// <summary>
/// call Convert(expr, call GetTypeFromHandle(ldtypetoken T))
/// =&gt;
/// expression.tree.cast T(expr)
/// A conversion from a small integer type to Int32 produces the operand unchanged,
/// because such values already occupy an I4 stack slot.
/// </summary>
Func<ILInstruction> ConvertCast(CallInstruction invocation, bool isChecked) Func<ILInstruction> ConvertCast(CallInstruction invocation, bool isChecked)
{ {
if (invocation.Arguments.Count < 2) if (invocation.Arguments.Count < 2)
@ -759,6 +928,15 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}; };
} }
/// <summary>
/// call Coalesce(leftExpr, rightExpr)
/// =&gt;
/// if.notnull(left, right)
/// The result type and NullCoalescingKind are picked from the inferred operand types: a
/// nullable left whose underlying type the right operand implicitly converts to gives
/// Nullable or NullableWithValueFallback, everything else gives Ref.
/// The three-argument overload, which carries an explicit conversion lambda, is not matched.
/// </summary>
Func<ILInstruction> ConvertCoalesce(CallInstruction invocation) Func<ILInstruction> ConvertCoalesce(CallInstruction invocation)
{ {
if (invocation.Arguments.Count != 2) if (invocation.Arguments.Count != 2)
@ -796,6 +974,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}; };
} }
/// <summary>
/// call Equal(left, right, ldc.i4 liftToNull, castclass System.Reflection.MethodInfo(call GetMethodFromHandle(ldmembertoken op_Equality)))
/// =&gt;
/// call op_Equality(left, right), lifted via LiftUserDefinedOperator when left is Nullable&lt;T&gt;
/// call Equal(left, right)
/// =&gt;
/// call op_Equality(left, right) for a user-defined operator found by the resolver, or for two
/// string operands; otherwise comp.i4(left == right), lifted[C#] when left is Nullable&lt;T&gt;.
/// Equal stands for whichever factory kind selects: NotEqual, LessThan, GreaterThan, ...
/// </summary>
Func<ILInstruction> ConvertComparison(CallInstruction invocation, ComparisonKind kind) Func<ILInstruction> ConvertComparison(CallInstruction invocation, ComparisonKind kind)
{ {
if (invocation.Arguments.Count < 2) if (invocation.Arguments.Count < 2)
@ -857,6 +1045,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}; };
} }
/// <summary>
/// call Condition(conditionExpr, trueExpr, falseExpr)
/// =&gt;
/// if (condition) trueValue else falseValue
/// The builder bails out unless the condition infers to bool and both branches infer to types
/// that are equivalent under type erasure; the true branch's type becomes the result type.
/// </summary>
Func<ILInstruction> ConvertCondition(CallInstruction invocation) Func<ILInstruction> ConvertCondition(CallInstruction invocation)
{ {
if (invocation.Arguments.Count != 3) if (invocation.Arguments.Count != 3)
@ -886,6 +1081,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}; };
} }
/// <summary>
/// call Constant(box T(value), call GetTypeFromHandle(ldtypetoken T))
/// =&gt;
/// value, or expression.tree.cast T(value) when T is an enum or bool
/// call Constant(ldstr "a" / ldnull / call GetTypeFromHandle(ldtypetoken X) / ldloc displayClass)
/// =&gt;
/// the reference itself; only value-type constants are boxed.
/// Roslyn emits the two-argument Constant(object, Type) overload; the legacy .NET Framework
/// csc uses the one-argument Constant(object) overload for display-class instances.
/// </summary>
Func<ILInstruction> ConvertConstant(CallInstruction invocation) Func<ILInstruction> ConvertConstant(CallInstruction invocation)
{ {
if (!MatchConstantCall(invocation, out var value)) if (!MatchConstantCall(invocation, out var value))
@ -912,6 +1117,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
/// <summary>
/// call ElementInit(castclass System.Reflection.MethodInfo(call GetMethodFromHandle(ldmembertoken Add)),
/// block ArrayInitializer { newarr Expression + one stobj per argument })
/// =&gt;
/// callvirt Add(args), or call Add(args) for a static method, with no target argument yet;
/// ConvertListInit inserts the collection instance at index 0.
/// </summary>
Func<ILInstruction> ConvertElementInit(CallInstruction invocation) Func<ILInstruction> ConvertElementInit(CallInstruction invocation)
{ {
if (invocation.Arguments.Count != 2) if (invocation.Arguments.Count != 2)
@ -940,6 +1152,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return BuildCall; return BuildCall;
} }
/// <summary>
/// call Field(ldnull, call GetFieldFromHandle(ldmembertoken F))
/// =&gt;
/// ldobj T(ldsflda F)
/// call Field(targetExpr, call GetFieldFromHandle(ldmembertoken F))
/// =&gt;
/// ldobj T(delayex.ldflda F(target)), with target wrapped in addressof when the declaring
/// type is a value type.
/// A by-ref typeHint on a field whose type is not by-ref-like drops the ldobj, so the field
/// address itself is produced.
/// </summary>
Func<ILInstruction> ConvertField(CallInstruction invocation, IType typeHint) Func<ILInstruction> ConvertField(CallInstruction invocation, IType typeHint)
{ {
if (invocation.Arguments.Count != 2) if (invocation.Arguments.Count != 2)
@ -953,11 +1176,6 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
if (!MatchGetFieldFromHandle(invocation.Arguments[1], out var member)) if (!MatchGetFieldFromHandle(invocation.Arguments[1], out var member))
return null; return null;
IType type = member.ReturnType;
if (typeHint.SkipModifiers() is ByReferenceType && !member.ReturnType.IsByRefLike)
{
type = typeHint;
}
return BuildField; return BuildField;
ILInstruction BuildField() ILInstruction BuildField()
@ -987,6 +1205,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
/// <summary>
/// call Invoke(targetExpr, block ArrayInitializer { newarr Expression + one stobj per argument })
/// =&gt;
/// callvirt Invoke(target, args)
/// The invoke method comes from the delegate type the target infers to; the builder bails out
/// if that type has none, or if an argument fails to convert.
/// </summary>
Func<ILInstruction> ConvertInvoke(CallInstruction invocation) Func<ILInstruction> ConvertInvoke(CallInstruction invocation)
{ {
if (invocation.Arguments.Count != 2) if (invocation.Arguments.Count != 2)
@ -1016,6 +1241,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return BuildCall; return BuildCall;
} }
/// <summary>
/// call ListInit(call New(...), block ArrayInitializer { call ElementInit(addMethod, args), ... })
/// or, with the add-method handle passed separately:
/// call ListInit(call New(...), addMethod, block ArrayInitializer { args })
/// =>
/// Block (CollectionInitializer) {
/// stloc initializer(newobj ctor(...))
/// callvirt Add(ldloc initializer, args) // one per element
/// final: ldloc initializer
/// }
/// </summary>
Func<ILInstruction> ConvertListInit(CallInstruction invocation) Func<ILInstruction> ConvertListInit(CallInstruction invocation)
{ {
if (invocation.Arguments.Count < 2) if (invocation.Arguments.Count < 2)
@ -1072,6 +1308,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return BuildBlock; return BuildBlock;
} }
/// <summary>
/// call AndAlso(left, right) / call OrElse(left, right)
/// =>
/// if (left) right else ldc.i4 0 / if (left) ldc.i4 1 else right
///
/// call AndAlso(left, right, method)
/// call AndAlso(left, right, ldc.i4 liftToNull, method)
/// =>
/// call method(left, right); the four-argument form lifts the user-defined operator
/// if the left operand infers to Nullable&lt;T&gt;.
/// </summary>
Func<ILInstruction> ConvertLogicOperator(CallInstruction invocation, bool and) Func<ILInstruction> ConvertLogicOperator(CallInstruction invocation, bool and)
{ {
if (invocation.Arguments.Count < 2) if (invocation.Arguments.Count < 2)
@ -1085,14 +1332,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms
IMember method; IMember method;
switch (invocation.Arguments.Count) switch (invocation.Arguments.Count)
{ {
// call AndAlso(left, right): built-in operator
case 2: case 2:
return () => and ? IfInstruction.LogicAnd(left(), right(), context.TypeSystem) : IfInstruction.LogicOr(left(), right(), context.TypeSystem); return () => and ? IfInstruction.LogicAnd(left(), right(), context.TypeSystem) : IfInstruction.LogicOr(left(), right(), context.TypeSystem);
// call AndAlso(left, right, methodInfo): user-defined operator
case 3: case 3:
if (!MatchGetMethodFromHandle(invocation.Arguments[2], out method)) if (!MatchGetMethodFromHandle(invocation.Arguments[2], out method))
return null; return null;
return () => new Call((IMethod)method) { return () => new Call((IMethod)method) {
Arguments = { left(), right() } Arguments = { left(), right() }
}; };
// call AndAlso(left, right, ldc.i4 liftToNull, methodInfo): AndAlso and OrElse
// declare no such overload
case 4: case 4:
if (!invocation.Arguments[2].MatchLdcI4(out _)) if (!invocation.Arguments[2].MatchLdcI4(out _))
return null; return null;
@ -1115,6 +1366,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
/// <summary>
/// call MemberInit(call New(...), block ArrayInitializer { call Bind(member, value), ... })
/// =>
/// Block (CollectionInitializer) {
/// stloc initializer(newobj ctor(...))
/// callvirt set_Member(ldloc initializer, value) // stobj for field bindings
/// final: ldloc initializer
/// }
/// Only Expression.Bind elements are supported; any other binding kind fails the match.
/// </summary>
Func<ILInstruction> ConvertMemberInit(CallInstruction invocation) Func<ILInstruction> ConvertMemberInit(CallInstruction invocation)
{ {
if (invocation.Arguments.Count != 2) if (invocation.Arguments.Count != 2)
@ -1162,6 +1423,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return BuildBlock; return BuildBlock;
} }
/// <summary>
/// call NewArrayBounds(call GetTypeFromHandle(ldtypetoken T), block ArrayInitializer { bounds })
/// =>
/// newarr T(bounds)
/// </summary>
Func<ILInstruction> ConvertNewArrayBounds(CallInstruction invocation) Func<ILInstruction> ConvertNewArrayBounds(CallInstruction invocation)
{ {
if (invocation.Arguments.Count != 2) if (invocation.Arguments.Count != 2)
@ -1183,6 +1449,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return () => new NewArr(type, indices.SelectArray(f => f())); return () => new NewArr(type, indices.SelectArray(f => f()));
} }
/// <summary>
/// call NewArrayInit(call GetTypeFromHandle(ldtypetoken T), block ArrayInitializer { values })
/// =>
/// Block (ArrayInitializer) {
/// stloc initializer(newarr T(ldc.i4 n))
/// stobj T(delayex.ldelema T(ldloc initializer, ldc.i4 i), value) // one per element
/// final: ldloc initializer
/// }
/// An empty value list produces a bare newarr T(ldc.i4 0) instead of a block.
/// </summary>
Func<ILInstruction> ConvertNewArrayInit(CallInstruction invocation) Func<ILInstruction> ConvertNewArrayInit(CallInstruction invocation)
{ {
if (invocation.Arguments.Count != 2) if (invocation.Arguments.Count != 2)
@ -1222,6 +1498,15 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return BuildInitializer; return BuildInitializer;
} }
/// <summary>
/// Matches the constructor named by a call to Expression.New; produces no ILAst.
/// call New(call GetTypeFromHandle(ldtypetoken T)) -&gt; the parameterless constructor of T
/// call New(ctorInfo)
/// call New(ctorInfo, block ArrayInitializer { args })
/// call New(ctorInfo, block ArrayInitializer { args }, block ArrayInitializer { members })
/// -&gt; the constructor named by ctorInfo, which is
/// castclass ConstructorInfo(call GetMethodFromHandle(ldmembertoken .ctor, ldtypetoken T)).
/// </summary>
bool MatchNew(CallInstruction invocation, out IMethod ctor) bool MatchNew(CallInstruction invocation, out IMethod ctor)
{ {
ctor = null; ctor = null;
@ -1229,6 +1514,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return false; return false;
switch (invocation.Arguments.Count) switch (invocation.Arguments.Count)
{ {
// call New(typeHandle) or call New(constructorInfo)
case 1: case 1:
if (MatchGetTypeFromHandle(invocation.Arguments[0], out var type)) if (MatchGetTypeFromHandle(invocation.Arguments[0], out var type))
{ {
@ -1241,6 +1527,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return true; return true;
} }
return false; return false;
// call New(constructorInfo, argumentList[, memberList])
case 2: case 2:
case 3: case 3:
if (!MatchGetConstructorFromHandle(invocation.Arguments[0], out member)) if (!MatchGetConstructorFromHandle(invocation.Arguments[0], out member))
@ -1252,10 +1539,21 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
/// <summary>
/// call New(call GetTypeFromHandle(ldtypetoken T)) / call New(ctorInfo)
/// => newobj ctor()
/// call New(ctorInfo, block ArrayInitializer { args })
/// => newobj ctor(args)
/// call New(ctorInfo, block ArrayInitializer { args }, block ArrayInitializer { members })
/// => newobj ctor(args); the member list, which names the anonymous type's property
/// accessors, has no ILAst equivalent and is dropped.
/// ctorInfo is castclass ConstructorInfo(call GetMethodFromHandle(ldmembertoken .ctor, ldtypetoken T)).
/// </summary>
Func<ILInstruction> ConvertNewObject(CallInstruction invocation) Func<ILInstruction> ConvertNewObject(CallInstruction invocation)
{ {
switch (invocation.Arguments.Count) switch (invocation.Arguments.Count)
{ {
// call New(typeHandle) or call New(constructorInfo): parameterless constructor
case 1: case 1:
if (MatchGetTypeFromHandle(invocation.Arguments[0], out var type)) if (MatchGetTypeFromHandle(invocation.Arguments[0], out var type))
{ {
@ -1269,6 +1567,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return () => new NewObj((IMethod)member); return () => new NewObj((IMethod)member);
} }
return null; return null;
// call New(constructorInfo, argumentList)
case 2: case 2:
if (!MatchGetConstructorFromHandle(invocation.Arguments[0], out member)) if (!MatchGetConstructorFromHandle(invocation.Arguments[0], out member))
return null; return null;
@ -1279,6 +1578,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
if (convertedArguments == null) if (convertedArguments == null)
return null; return null;
return () => BuildNewObj(method, convertedArguments); return () => BuildNewObj(method, convertedArguments);
// call New(constructorInfo, argumentList, memberList): anonymous types
case 3: case 3:
if (!MatchGetConstructorFromHandle(invocation.Arguments[0], out member)) if (!MatchGetConstructorFromHandle(invocation.Arguments[0], out member))
return null; return null;
@ -1301,6 +1601,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return null; return null;
} }
/// <summary>
/// call Not(value) / call OnesComplement(value)
/// =>
/// logic.not(value) if value infers to bool, otherwise bit.not(value) on the
/// underlying type's stack type; both are lifted if the inferred type is Nullable&lt;T&gt;.
///
/// call Not(value, castclass MethodInfo(call GetMethodFromHandle(ldmembertoken op_LogicalNot, ldtypetoken T)))
/// =>
/// call op_LogicalNot(value)
/// </summary>
Func<ILInstruction> ConvertNotOperator(CallInstruction invocation) Func<ILInstruction> ConvertNotOperator(CallInstruction invocation)
{ {
if (invocation.Arguments.Count < 1) if (invocation.Arguments.Count < 1)
@ -1310,6 +1620,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return null; return null;
switch (invocation.Arguments.Count) switch (invocation.Arguments.Count)
{ {
// call Not(expression): built-in operator
case 1: case 1:
return () => { return () => {
var argumentInst = argument(); var argumentInst = argument();
@ -1322,6 +1633,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
? Comp.LogicNot(argumentInst, isLifted) ? Comp.LogicNot(argumentInst, isLifted)
: (ILInstruction)new BitNot(argumentInst, isLifted, underlyingType.GetStackType()); : (ILInstruction)new BitNot(argumentInst, isLifted, underlyingType.GetStackType());
}; };
// call Not(expression, methodInfo): user-defined op_LogicalNot or op_OnesComplement
case 2: case 2:
if (!MatchGetMethodFromHandle(invocation.Arguments[1], out var method)) if (!MatchGetMethodFromHandle(invocation.Arguments[1], out var method))
return null; return null;
@ -1333,6 +1645,15 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
/// <summary>
/// call Property(target, castclass MethodInfo(call GetMethodFromHandle(ldmembertoken get_X, ldtypetoken T)))
/// call Property(target, accessorInfo, block ArrayInitializer { indices })
/// =>
/// callvirt get_X(target, indices)
/// A static accessor uses call instead of callvirt; ldnull as the first argument
/// emits no target argument. The target is adapted to the accessor's this-pointer
/// stack type (address-of or box for value types).
/// </summary>
Func<ILInstruction> ConvertProperty(CallInstruction invocation) Func<ILInstruction> ConvertProperty(CallInstruction invocation)
{ {
if (invocation.Arguments.Count < 2) if (invocation.Arguments.Count < 2)
@ -1378,6 +1699,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return BuildProperty; return BuildProperty;
} }
/// <summary>
/// call TypeAs(value, call GetTypeFromHandle(ldtypetoken T))
/// =>
/// isinst T(value)
/// For T = Nullable&lt;U&gt; the result is wrapped in unbox.any T, because isinst on a
/// nullable type tests for boxed U per ECMA-335, III.4.6.
/// </summary>
Func<ILInstruction> ConvertTypeAs(CallInstruction invocation) Func<ILInstruction> ConvertTypeAs(CallInstruction invocation)
{ {
if (invocation.Arguments.Count != 2) if (invocation.Arguments.Count != 2)
@ -1399,6 +1727,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return BuildTypeAs; return BuildTypeAs;
} }
/// <summary>
/// call TypeIs(value, call GetTypeFromHandle(ldtypetoken T))
/// =>
/// comp.obj(isinst T(value) != ldnull)
/// </summary>
Func<ILInstruction> ConvertTypeIs(CallInstruction invocation) Func<ILInstruction> ConvertTypeIs(CallInstruction invocation)
{ {
if (invocation.Arguments.Count != 2) if (invocation.Arguments.Count != 2)
@ -1412,6 +1745,22 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return null; return null;
} }
/// <summary>
/// call Negate(argumentExpr)
/// =&gt;
/// binary.sub.i4(ldc.i4 0, argument)
///
/// The built-in form has no MethodInfo: the operation is expressed as a binary
/// instruction with a zero literal on the left. The literal is picked in the returned
/// builder from the stack type inferred for the converted argument: ldc.i4 0 for I4,
/// ldc.i8 0 for I8, conv i4-&gt;i for I, ldc.f4/ldc.f8 0 for F4/F8 and ldc.decimal 0
/// for System.Decimal; any other stack type is rejected. A nullable argument type
/// produces a lifted instruction over the underlying type.
///
/// call Negate(argumentExpr, castclass System.Reflection.MethodInfo(call GetMethodFromHandle(ldmembertoken op_UnaryNegation)))
/// =&gt;
/// call op_UnaryNegation(argument)
/// </summary>
Func<ILInstruction> ConvertUnaryNumericOperator(CallInstruction invocation, BinaryNumericOperator op, bool? isChecked = null) Func<ILInstruction> ConvertUnaryNumericOperator(CallInstruction invocation, BinaryNumericOperator op, bool? isChecked = null)
{ {
if (invocation.Arguments.Count < 1) if (invocation.Arguments.Count < 1)
@ -1421,6 +1770,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return null; return null;
switch (invocation.Arguments.Count) switch (invocation.Arguments.Count)
{ {
// call Negate(expression): built-in operator
case 1: case 1:
return () => { return () => {
var argumentInst = argument(); var argumentInst = argument();
@ -1460,6 +1810,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
argumentType.GetSign(), argumentType.GetSign(),
isLifted: NullableType.IsNullable(argumentType)); isLifted: NullableType.IsNullable(argumentType));
}; };
// call Negate(expression, methodInfo): user-defined op_UnaryNegation
case 2: case 2:
if (!MatchGetMethodFromHandle(invocation.Arguments[1], out var method)) if (!MatchGetMethodFromHandle(invocation.Arguments[1], out var method))
return null; return null;
@ -1470,6 +1821,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return null; return null;
} }
/// <summary>
/// Post-processes the value operand of a converted Expression.Constant call;
/// <paramref name="context"/> is the surrounding Expression call instruction.
/// A ldloc of an expression-tree ParameterExpression variable is mapped to the
/// ILVariable generated for that parameter, but only where a constant may legally
/// stand in for it: under Expression.Call with an integer stack type it becomes
/// ldloca of the mapped variable, an unmapped variable is cloned unchanged, and any
/// other mapped use is rejected (null).
/// A ldloc of a closure reference is returned as is, after marking the variable as
/// a display-class local and registering it as a captured variable of the enclosing
/// ILFunction. Everything else is cloned.
/// </summary>
ILInstruction ConvertValue(ILInstruction value, ILInstruction context) ILInstruction ConvertValue(ILInstruction value, ILInstruction context)
{ {
switch (value) switch (value)
@ -1511,6 +1874,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
/// <summary>
/// Whether the variable has a single store of the form stloc v(newobj DisplayClass..ctor())
/// that TransformDisplayClassUsage recognizes as a potential closure.
/// </summary>
bool IsClosureReference(ILVariable variable) bool IsClosureReference(ILVariable variable)
{ {
if (!variable.IsSingleDefinition || !(variable.StoreInstructions.SingleOrDefault() is StLoc store)) if (!variable.IsSingleDefinition || !(variable.StoreInstructions.SingleOrDefault() is StLoc store))
@ -1520,11 +1887,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return TransformDisplayClassUsage.IsPotentialClosure(this.context, newObj); return TransformDisplayClassUsage.IsPotentialClosure(this.context, newObj);
} }
/// <summary>
/// Whether the variable holds a System.Linq.Expressions.ParameterExpression.
/// </summary>
bool IsExpressionTreeParameter(ILVariable variable) bool IsExpressionTreeParameter(ILVariable variable)
{ {
return variable.Type.FullName == "System.Linq.Expressions.ParameterExpression"; return variable.Type.FullName == "System.Linq.Expressions.ParameterExpression";
} }
/// <summary>
/// call GetTypeFromHandle(ldtypetoken T)
/// Hands back T.
/// </summary>
internal static bool MatchGetTypeFromHandle(ILInstruction inst, out IType type) internal static bool MatchGetTypeFromHandle(ILInstruction inst, out IType type)
{ {
type = null; type = null;
@ -1534,6 +1908,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms
&& getTypeCall.Arguments[0].MatchLdTypeToken(out type); && getTypeCall.Arguments[0].MatchLdTypeToken(out type);
} }
/// <summary>
/// castclass System.Reflection.MethodInfo(call GetMethodFromHandle(ldmembertoken M))
/// Hands back the method M; see MatchFromHandleParameterList for the accepted
/// argument lists of the GetMethodFromHandle call.
/// </summary>
bool MatchGetMethodFromHandle(ILInstruction inst, out IMember member) bool MatchGetMethodFromHandle(ILInstruction inst, out IMember member)
{ {
member = null; member = null;
@ -1547,6 +1926,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return MatchFromHandleParameterList(call, out member); return MatchFromHandleParameterList(call, out member);
} }
/// <summary>
/// castclass System.Reflection.ConstructorInfo(call GetMethodFromHandle(ldmembertoken C))
/// Hands back the constructor C; see MatchFromHandleParameterList for the accepted
/// argument lists of the GetMethodFromHandle call.
/// </summary>
bool MatchGetConstructorFromHandle(ILInstruction inst, out IMember member) bool MatchGetConstructorFromHandle(ILInstruction inst, out IMember member)
{ {
member = null; member = null;
@ -1560,6 +1944,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return MatchFromHandleParameterList(call, out member); return MatchFromHandleParameterList(call, out member);
} }
/// <summary>
/// call GetFieldFromHandle(ldmembertoken F)
/// Hands back the field F; see MatchFromHandleParameterList for the accepted
/// argument lists of the call.
/// </summary>
bool MatchGetFieldFromHandle(ILInstruction inst, out IMember member) bool MatchGetFieldFromHandle(ILInstruction inst, out IMember member)
{ {
member = null; member = null;
@ -1568,6 +1957,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return MatchFromHandleParameterList(call, out member); return MatchFromHandleParameterList(call, out member);
} }
/// <summary>
/// Accepts the argument list of a GetMethodFromHandle/GetFieldFromHandle call in both
/// its overloads: (ldmembertoken M), and (ldmembertoken M, ldtypetoken T) for a member
/// of a generic type. Hands back M; the declaring-type token is only checked for shape,
/// because the member token already carries the specialized member.
/// </summary>
static bool MatchFromHandleParameterList(CallInstruction call, out IMember member) static bool MatchFromHandleParameterList(CallInstruction call, out IMember member)
{ {
member = null; member = null;
@ -1589,6 +1984,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return true; return true;
} }
/// <summary>
/// Block (ArrayInitializer) {
/// stloc S(newarr T(ldc.i4 n))
/// stobj T(ldelema T(ldloc S, ldc.i4 0), value0)
/// ...
/// stobj T(ldelema T(ldloc S, ldc.i4 n-1), value_n-1)
/// final: ldloc S
/// }
/// Hands back the element values in index order; the indices must be the dense
/// sequence 0..n-1. An empty list is also matched outside a block, as
/// newarr ParameterExpression/Expression(ldc.i4 0) or call Array.Empty().
/// </summary>
bool MatchArgumentList(ILInstruction inst, out IList<ILInstruction> arguments) bool MatchArgumentList(ILInstruction inst, out IList<ILInstruction> arguments)
{ {
arguments = null; arguments = null;

Loading…
Cancel
Save