From 154d60e2a3e149024278475c5b431657690f949b Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 5 Sep 2026 10:06:19 +0200 Subject: [PATCH] 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 --- .../IL/Transforms/TransformExpressionTrees.cs | 417 +++++++++++++++++- 1 file changed, 412 insertions(+), 5 deletions(-) diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs index dc4ce8a46..e191e807c 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs @@ -39,6 +39,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms { /// /// Returns true if the instruction matches the pattern for Expression.Lambda calls. + /// + /// call Lambda(<body>, <parameter array>) + /// + /// where <parameter array> is either an empty parameter list (see + /// ) or a Block of kind ArrayInitializer. + /// This is only a cheap pre-filter, the actual conversion is done by + /// . /// static bool MightBeExpressionTree(ILInstruction inst, ILInstruction stmt) { @@ -53,6 +60,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms return true; } + /// + /// 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. + /// static bool IsEmptyParameterList(ILInstruction inst) { 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; } + /// + /// stloc v(call Parameter(call GetTypeFromHandle(ldtypetoken T), ldstr "name")) + /// => + /// 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. + /// bool MatchParameterVariableAssignment(ILInstruction expr, out ILVariable parameterReferenceVar, out IType type, out string name) { // stloc(v, call(Expression::Parameter, call(Type::GetTypeFromHandle, ldtoken(...)), ldstr(...))) @@ -97,6 +118,15 @@ namespace ICSharpCode.Decompiler.IL.Transforms CSharpConversions conversions; CSharpResolver resolver; + /// + /// 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 + /// . On success the parameter declarations + /// consumed by the converted tree are removed from the block. + /// public void Run(Block block, int pos, StatementTransformContext context) { if (!context.Settings.ExpressionTrees) @@ -125,6 +155,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// Searches instruction for the first + /// + /// call Lambda(<body>, <parameter array>) + /// + /// and replaces it with the ILFunction built by . + /// Nested control-flow blocks are not searched. Returns true if a tree was converted. + /// bool TryConvertExpressionTree(ILInstruction instruction, ILInstruction statement) { if (MightBeExpressionTree(instruction, statement)) @@ -154,6 +192,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// /// Converts a Expression.Lambda call into an ILFunction. /// If the conversion fails, null is returned. + /// + /// call Lambda(<body>, Block (ArrayInitializer) { stobj System.Object(delayex.ldelema System.Object(ldloc S, ldc.i4 0), ldloc V_0), ... }) + /// => + /// ILFunction(<parameters>) { BlockContainer { Block { leave (<converted body>) } } } + /// + /// The parameters are read from the array initializer by . + /// The call must return Expression<TDelegate>; 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. /// Func ConvertLambda(CallInstruction instruction) { @@ -202,6 +250,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// call Quote(<lambda>) + /// => + /// <converted lambda> + /// + /// 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 . + /// Func ConvertQuote(CallInstruction invocation) { if (invocation.Arguments.Count != 1) @@ -231,12 +289,30 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// Sets DelegateType and Kind of lambda from the return type of call: a return type + /// Expression<TDelegate> gives ILFunctionKind.ExpressionTree, any other type gives + /// ILFunctionKind.Delegate. + /// void SetExpressionTreeFlag(ILFunction lambda, CallInstruction call) { lambda.Kind = IsExpressionTree(call.Method.ReturnType) ? ILFunctionKind.ExpressionTree : ILFunctionKind.Delegate; lambda.DelegateType = call.Method.ReturnType; } + /// + /// 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), ... } + /// => + /// one IParameter and one ILVariable of kind Parameter per element, using the type + /// and name recorded for V_i by . + /// An empty parameter list (see ) yields none. + /// + /// Each ParameterExpression variable enters the mapping only once; its defining + /// stloc is queued for removal. + /// bool ReadParameters(ILInstruction initializer, IList parameters, IList parameterVariables, ITypeResolveContext resolveContext) { switch (initializer) @@ -270,6 +346,21 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// Converts one node of the expression tree into a Func<ILInstruction> building the + /// equivalent ILAst, or null if the node cannot be converted: + /// + /// call <name>(...) on System.Linq.Expressions.Expression => the result of the + /// Convert* method for <name>, e.g. call Add(a, b) => binary.numeric.add(a, b). + /// ILFunction (an already converted nested lambda) => the same function, with an + /// expression tree DelegateType unwrapped to TDelegate and kind set to Delegate. + /// ldloc v, v a ParameterExpression => 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. + /// Func ConvertInstruction(ILInstruction instruction, IType typeHint = null) { var inst = Convert(); @@ -437,10 +528,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// Returns true for System.Linq.Expressions.Expression<T>. + /// bool IsExpressionTree(IType delegateType) => delegateType is ParameterizedType pt && pt.FullName == "System.Linq.Expressions.Expression" && pt.TypeArguments.Count == 1; + /// + /// Returns T for System.Linq.Expressions.Expression<T>; any other type is returned unchanged. + /// IType UnwrapExpressionTree(IType delegateType) { 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; } + /// + /// call ArrayIndex(array, index) + /// call ArrayIndex(array, argumentList) // multi-dimensional arrays + /// => + /// 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. + /// Func ConvertArrayIndex(CallInstruction invocation) { if (invocation.Arguments.Count != 2) @@ -480,6 +585,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms return Convert; } + /// + /// call ArrayLength(array) + /// => + /// ldlen.i4(array) + /// Func ConvertArrayLength(CallInstruction invocation) { if (invocation.Arguments.Count != 1) @@ -490,6 +600,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms return () => new LdLen(StackType.I4, converted()); } + /// + /// 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 + /// => + /// 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. + /// Func ConvertBinaryNumericOperator(CallInstruction invocation, BinaryNumericOperator op, string operatorName, bool? isChecked = null) { if (invocation.Arguments.Count < 2) @@ -504,6 +626,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms IMember method; switch (invocation.Arguments.Count) { + // call Add(left, right): built-in operator, or the operator method of decimal case 2: return () => { var leftInst = left(); @@ -538,12 +661,15 @@ namespace ICSharpCode.Decompiler.IL.Transforms leftType.GetSign(), isLifted: NullableType.IsNullable(leftType) || NullableType.IsNullable(rightType)); }; + // call Add(left, right, methodInfo): user-defined operator case 3: if (!MatchGetMethodFromHandle(invocation.Arguments[2], out method)) return null; return () => new Call((IMethod)method) { 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: if (!invocation.Arguments[2].MatchLdcI4(out _)) return null; @@ -566,6 +692,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// call Bind(castclass System.Reflection.MethodInfo(call GetMethodFromHandle(ldmembertoken set_P)), value) + /// call Bind(call GetFieldFromHandle(ldmembertoken F), value) + /// => + /// 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. + /// Func ConvertBind(CallInstruction invocation) { if (invocation.Arguments.Count != 2) @@ -605,6 +739,20 @@ namespace ICSharpCode.Decompiler.IL.Transforms return null; } + /// + /// call Call(MethodInfo, argumentList) // static method + /// call Call(target, MethodInfo, argumentList) // target is ldnull for static methods + /// => + /// 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 }) + /// => + /// 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. + /// Func ConvertCall(CallInstruction invocation) { if (invocation.Arguments.Count < 2) @@ -673,6 +821,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms return BuildCall; } + /// + /// 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. + /// ILInstruction PrepareCallTarget(IType expectedType, ILInstruction target, IType targetType) { ILInstruction result; @@ -718,6 +873,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms return result; } + /// + /// Returns the value of call Constant(value, typeToken); any other instruction is returned unchanged. + /// ILInstruction UnpackConstant(ILInstruction inst) { 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]; } + /// + /// Converts each argument using the corresponding parameter type of method as type hint. + /// Returns null if any argument cannot be converted. + /// Func[] ConvertCallArguments(IList arguments, IMethod method) { var converted = new Func[arguments.Count]; @@ -740,6 +902,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms return converted; } + /// + /// call Convert(expr, call GetTypeFromHandle(ldtypetoken T)) + /// => + /// 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. + /// Func ConvertCast(CallInstruction invocation, bool isChecked) { if (invocation.Arguments.Count < 2) @@ -759,6 +928,15 @@ namespace ICSharpCode.Decompiler.IL.Transforms }; } + /// + /// call Coalesce(leftExpr, rightExpr) + /// => + /// 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. + /// Func ConvertCoalesce(CallInstruction invocation) { if (invocation.Arguments.Count != 2) @@ -796,6 +974,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms }; } + /// + /// call Equal(left, right, ldc.i4 liftToNull, castclass System.Reflection.MethodInfo(call GetMethodFromHandle(ldmembertoken op_Equality))) + /// => + /// call op_Equality(left, right), lifted via LiftUserDefinedOperator when left is Nullable<T> + /// call Equal(left, right) + /// => + /// 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<T>. + /// Equal stands for whichever factory kind selects: NotEqual, LessThan, GreaterThan, ... + /// Func ConvertComparison(CallInstruction invocation, ComparisonKind kind) { if (invocation.Arguments.Count < 2) @@ -857,6 +1045,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms }; } + /// + /// call Condition(conditionExpr, trueExpr, falseExpr) + /// => + /// 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. + /// Func ConvertCondition(CallInstruction invocation) { if (invocation.Arguments.Count != 3) @@ -886,6 +1081,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms }; } + /// + /// call Constant(box T(value), call GetTypeFromHandle(ldtypetoken T)) + /// => + /// 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) + /// => + /// 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. + /// Func ConvertConstant(CallInstruction invocation) { if (!MatchConstantCall(invocation, out var value)) @@ -912,6 +1117,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// call ElementInit(castclass System.Reflection.MethodInfo(call GetMethodFromHandle(ldmembertoken Add)), + /// block ArrayInitializer { newarr Expression + one stobj per argument }) + /// => + /// callvirt Add(args), or call Add(args) for a static method, with no target argument yet; + /// ConvertListInit inserts the collection instance at index 0. + /// Func ConvertElementInit(CallInstruction invocation) { if (invocation.Arguments.Count != 2) @@ -940,6 +1152,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms return BuildCall; } + /// + /// call Field(ldnull, call GetFieldFromHandle(ldmembertoken F)) + /// => + /// ldobj T(ldsflda F) + /// call Field(targetExpr, call GetFieldFromHandle(ldmembertoken F)) + /// => + /// 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. + /// Func ConvertField(CallInstruction invocation, IType typeHint) { if (invocation.Arguments.Count != 2) @@ -953,11 +1176,6 @@ namespace ICSharpCode.Decompiler.IL.Transforms } if (!MatchGetFieldFromHandle(invocation.Arguments[1], out var member)) return null; - IType type = member.ReturnType; - if (typeHint.SkipModifiers() is ByReferenceType && !member.ReturnType.IsByRefLike) - { - type = typeHint; - } return BuildField; ILInstruction BuildField() @@ -987,6 +1205,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// call Invoke(targetExpr, block ArrayInitializer { newarr Expression + one stobj per argument }) + /// => + /// 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. + /// Func ConvertInvoke(CallInstruction invocation) { if (invocation.Arguments.Count != 2) @@ -1016,6 +1241,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms return BuildCall; } + /// + /// 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 + /// } + /// Func ConvertListInit(CallInstruction invocation) { if (invocation.Arguments.Count < 2) @@ -1072,6 +1308,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms return BuildBlock; } + /// + /// 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<T>. + /// Func ConvertLogicOperator(CallInstruction invocation, bool and) { if (invocation.Arguments.Count < 2) @@ -1085,14 +1332,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms IMember method; switch (invocation.Arguments.Count) { + // call AndAlso(left, right): built-in operator case 2: return () => and ? IfInstruction.LogicAnd(left(), right(), context.TypeSystem) : IfInstruction.LogicOr(left(), right(), context.TypeSystem); + // call AndAlso(left, right, methodInfo): user-defined operator case 3: if (!MatchGetMethodFromHandle(invocation.Arguments[2], out method)) return null; return () => new Call((IMethod)method) { Arguments = { left(), right() } }; + // call AndAlso(left, right, ldc.i4 liftToNull, methodInfo): AndAlso and OrElse + // declare no such overload case 4: if (!invocation.Arguments[2].MatchLdcI4(out _)) return null; @@ -1115,6 +1366,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// 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. + /// Func ConvertMemberInit(CallInstruction invocation) { if (invocation.Arguments.Count != 2) @@ -1162,6 +1423,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms return BuildBlock; } + /// + /// call NewArrayBounds(call GetTypeFromHandle(ldtypetoken T), block ArrayInitializer { bounds }) + /// => + /// newarr T(bounds) + /// Func ConvertNewArrayBounds(CallInstruction invocation) { if (invocation.Arguments.Count != 2) @@ -1183,6 +1449,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms return () => new NewArr(type, indices.SelectArray(f => f())); } + /// + /// 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. + /// Func ConvertNewArrayInit(CallInstruction invocation) { if (invocation.Arguments.Count != 2) @@ -1222,6 +1498,15 @@ namespace ICSharpCode.Decompiler.IL.Transforms return BuildInitializer; } + /// + /// Matches the constructor named by a call to Expression.New; produces no ILAst. + /// call New(call GetTypeFromHandle(ldtypetoken T)) -> the parameterless constructor of T + /// call New(ctorInfo) + /// call New(ctorInfo, block ArrayInitializer { args }) + /// call New(ctorInfo, block ArrayInitializer { args }, block ArrayInitializer { members }) + /// -> the constructor named by ctorInfo, which is + /// castclass ConstructorInfo(call GetMethodFromHandle(ldmembertoken .ctor, ldtypetoken T)). + /// bool MatchNew(CallInstruction invocation, out IMethod ctor) { ctor = null; @@ -1229,6 +1514,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms return false; switch (invocation.Arguments.Count) { + // call New(typeHandle) or call New(constructorInfo) case 1: if (MatchGetTypeFromHandle(invocation.Arguments[0], out var type)) { @@ -1241,6 +1527,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms return true; } return false; + // call New(constructorInfo, argumentList[, memberList]) case 2: case 3: if (!MatchGetConstructorFromHandle(invocation.Arguments[0], out member)) @@ -1252,10 +1539,21 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// 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)). + /// Func ConvertNewObject(CallInstruction invocation) { switch (invocation.Arguments.Count) { + // call New(typeHandle) or call New(constructorInfo): parameterless constructor case 1: if (MatchGetTypeFromHandle(invocation.Arguments[0], out var type)) { @@ -1269,6 +1567,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms return () => new NewObj((IMethod)member); } return null; + // call New(constructorInfo, argumentList) case 2: if (!MatchGetConstructorFromHandle(invocation.Arguments[0], out member)) return null; @@ -1279,6 +1578,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (convertedArguments == null) return null; return () => BuildNewObj(method, convertedArguments); + // call New(constructorInfo, argumentList, memberList): anonymous types case 3: if (!MatchGetConstructorFromHandle(invocation.Arguments[0], out member)) return null; @@ -1301,6 +1601,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms return null; } + /// + /// 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<T>. + /// + /// call Not(value, castclass MethodInfo(call GetMethodFromHandle(ldmembertoken op_LogicalNot, ldtypetoken T))) + /// => + /// call op_LogicalNot(value) + /// Func ConvertNotOperator(CallInstruction invocation) { if (invocation.Arguments.Count < 1) @@ -1310,6 +1620,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms return null; switch (invocation.Arguments.Count) { + // call Not(expression): built-in operator case 1: return () => { var argumentInst = argument(); @@ -1322,6 +1633,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms ? Comp.LogicNot(argumentInst, isLifted) : (ILInstruction)new BitNot(argumentInst, isLifted, underlyingType.GetStackType()); }; + // call Not(expression, methodInfo): user-defined op_LogicalNot or op_OnesComplement case 2: if (!MatchGetMethodFromHandle(invocation.Arguments[1], out var method)) return null; @@ -1333,6 +1645,15 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// 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). + /// Func ConvertProperty(CallInstruction invocation) { if (invocation.Arguments.Count < 2) @@ -1378,6 +1699,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms return BuildProperty; } + /// + /// call TypeAs(value, call GetTypeFromHandle(ldtypetoken T)) + /// => + /// isinst T(value) + /// For T = Nullable<U> the result is wrapped in unbox.any T, because isinst on a + /// nullable type tests for boxed U per ECMA-335, III.4.6. + /// Func ConvertTypeAs(CallInstruction invocation) { if (invocation.Arguments.Count != 2) @@ -1399,6 +1727,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms return BuildTypeAs; } + /// + /// call TypeIs(value, call GetTypeFromHandle(ldtypetoken T)) + /// => + /// comp.obj(isinst T(value) != ldnull) + /// Func ConvertTypeIs(CallInstruction invocation) { if (invocation.Arguments.Count != 2) @@ -1412,6 +1745,22 @@ namespace ICSharpCode.Decompiler.IL.Transforms return null; } + /// + /// call Negate(argumentExpr) + /// => + /// 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->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))) + /// => + /// call op_UnaryNegation(argument) + /// Func ConvertUnaryNumericOperator(CallInstruction invocation, BinaryNumericOperator op, bool? isChecked = null) { if (invocation.Arguments.Count < 1) @@ -1421,6 +1770,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms return null; switch (invocation.Arguments.Count) { + // call Negate(expression): built-in operator case 1: return () => { var argumentInst = argument(); @@ -1460,6 +1810,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms argumentType.GetSign(), isLifted: NullableType.IsNullable(argumentType)); }; + // call Negate(expression, methodInfo): user-defined op_UnaryNegation case 2: if (!MatchGetMethodFromHandle(invocation.Arguments[1], out var method)) return null; @@ -1470,6 +1821,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms return null; } + /// + /// Post-processes the value operand of a converted Expression.Constant call; + /// 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. + /// ILInstruction ConvertValue(ILInstruction value, ILInstruction context) { switch (value) @@ -1511,6 +1874,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// Whether the variable has a single store of the form stloc v(newobj DisplayClass..ctor()) + /// that TransformDisplayClassUsage recognizes as a potential closure. + /// bool IsClosureReference(ILVariable variable) { if (!variable.IsSingleDefinition || !(variable.StoreInstructions.SingleOrDefault() is StLoc store)) @@ -1520,11 +1887,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms return TransformDisplayClassUsage.IsPotentialClosure(this.context, newObj); } + /// + /// Whether the variable holds a System.Linq.Expressions.ParameterExpression. + /// bool IsExpressionTreeParameter(ILVariable variable) { return variable.Type.FullName == "System.Linq.Expressions.ParameterExpression"; } + /// + /// call GetTypeFromHandle(ldtypetoken T) + /// Hands back T. + /// internal static bool MatchGetTypeFromHandle(ILInstruction inst, out IType type) { type = null; @@ -1534,6 +1908,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms && getTypeCall.Arguments[0].MatchLdTypeToken(out type); } + /// + /// castclass System.Reflection.MethodInfo(call GetMethodFromHandle(ldmembertoken M)) + /// Hands back the method M; see MatchFromHandleParameterList for the accepted + /// argument lists of the GetMethodFromHandle call. + /// bool MatchGetMethodFromHandle(ILInstruction inst, out IMember member) { member = null; @@ -1547,6 +1926,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms return MatchFromHandleParameterList(call, out member); } + /// + /// castclass System.Reflection.ConstructorInfo(call GetMethodFromHandle(ldmembertoken C)) + /// Hands back the constructor C; see MatchFromHandleParameterList for the accepted + /// argument lists of the GetMethodFromHandle call. + /// bool MatchGetConstructorFromHandle(ILInstruction inst, out IMember member) { member = null; @@ -1560,6 +1944,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms return MatchFromHandleParameterList(call, out member); } + /// + /// call GetFieldFromHandle(ldmembertoken F) + /// Hands back the field F; see MatchFromHandleParameterList for the accepted + /// argument lists of the call. + /// bool MatchGetFieldFromHandle(ILInstruction inst, out IMember member) { member = null; @@ -1568,6 +1957,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms return MatchFromHandleParameterList(call, out member); } + /// + /// 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. + /// static bool MatchFromHandleParameterList(CallInstruction call, out IMember member) { member = null; @@ -1589,6 +1984,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms return true; } + /// + /// 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(). + /// bool MatchArgumentList(ILInstruction inst, out IList arguments) { arguments = null;