Browse Source

Fix #3729: recognize value-type ctor call on unresolved type

A call to a value-type constructor is rewritten into
"stobj(target, newobj ...)" because "Struct.ctor(target, ...)" has no C#
equivalent. The rewrite keyed on TypeKind.Struct, so a struct from a
missing assembly resolved as TypeKind.Unknown, fell through to the
ordinary call path and produced a stack-type mismatch.

Metadata cannot settle the question: a TypeRef parent carries no valuetype
bit. The receiver can, though - a constructor invoked with "call" on an
address is a shape only a value type has - so the unresolved case follows
the receiver's stack type and steps aside where metadata does say the type
is a reference type. Reading the receiver has to leave it on the stack for
PrepareArguments, hence the depth-indexed peek.

Assisted-by: Claude:claude-opus-5:Claude Code
pull/4000/head
Siegfried Pammer 1 month ago
parent
commit
e16a5523e9
  1. 46
      ICSharpCode.Decompiler/IL/ILReader.cs

46
ICSharpCode.Decompiler/IL/ILReader.cs

@ -1276,14 +1276,24 @@ namespace ICSharpCode.Decompiler.IL @@ -1276,14 +1276,24 @@ namespace ICSharpCode.Decompiler.IL
}
}
StackType PeekStackType()
StackType PeekStackType() => PeekStackTypeAtDepth(0);
StackType PeekStackTypeAtDepth(int depth)
{
if (expressionStack.Count > 0)
return expressionStack.Last().ResultType;
if (currentStack.IsEmpty)
// depth 0 = top of stack, depth N = N-th item below the top.
if (depth < expressionStack.Count)
return expressionStack[expressionStack.Count - 1 - depth].ResultType;
int skip = depth - expressionStack.Count;
var stack = currentStack;
for (int i = 0; i < skip; i++)
{
if (stack.IsEmpty)
return StackType.Unknown;
stack = stack.Pop();
}
if (stack.IsEmpty)
return StackType.Unknown;
else
return currentStack.Peek().StackType;
return stack.Peek().StackType;
}
sealed class CollectStackVariablesVisitor : ILVisitor<ILInstruction>
@ -1765,14 +1775,17 @@ namespace ICSharpCode.Decompiler.IL @@ -1765,14 +1775,17 @@ namespace ICSharpCode.Decompiler.IL
Warn("Unknown method called on array type: " + method.Name);
goto default;
}
case TypeKind.Struct when method.IsConstructor && !method.IsStatic && opCode == OpCode.Call
&& method.ReturnType.Kind == TypeKind.Void:
case TypeKind.Struct when IsValueTypeCtorCall():
case TypeKind.Unknown when IsValueTypeCtorCall() && ReceiverLooksLikeValueTypeTarget():
{
// "call Struct.ctor(target, ...)" doesn't exist in C#,
// the next best equivalent is an assignment `*target = new Struct(...);`.
// So we represent this call as "stobj Struct(target, newobj Struct.ctor(...))".
// This needs to happen early (not as a transform) because the StObj.TargetSlot has
// restricted inlining (doesn't accept ldflda when exceptions aren't delayed).
// The declaring type's kind is unavailable when its assembly is missing, so the
// receiver decides: a constructor invoked with "call" on an address is the shape
// only a value type can have.
arguments = PrepareArguments(firstArgumentIsStObjTarget: true);
var newobj = new NewObj(method);
newobj.ILStackWasEmpty = CurrentStackIsEmpty();
@ -1791,6 +1804,23 @@ namespace ICSharpCode.Decompiler.IL @@ -1791,6 +1804,23 @@ namespace ICSharpCode.Decompiler.IL
return call;
}
// A value type's constructor is invoked with "call" on the address of the target,
// unlike a reference type's, which is invoked with "newobj".
bool IsValueTypeCtorCall()
{
return method.IsConstructor && !method.IsStatic && opCode == OpCode.Call
&& method.ReturnType.Kind == TypeKind.Void;
}
// Used when the declaring type could not be resolved, so its kind is unknown: the
// receiver is an address (a managed reference, or a pointer in unsafe code), and
// metadata that does say the type is a reference type rules the rewrite out.
bool ReceiverLooksLikeValueTypeTarget()
{
return PeekStackTypeAtDepth(method.Parameters.Count) is StackType.Ref or StackType.I
&& method.DeclaringType.IsReferenceType != true;
}
ILInstruction[] PrepareArguments(bool firstArgumentIsStObjTarget)
{
int firstArgument = (opCode != OpCode.NewObj && !method.IsStatic) ? 1 : 0;

Loading…
Cancel
Save