Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/ILInspector.Decompiler.Tests/IlProjectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ public void Raw_ResolvesCallOperandToName()
Assert.DoesNotContain("0x", output);
}

[Fact]
public void Typed_AnnotatesPerInstructionStackTypes()
{
// LengthOf is `s.Length`: ldarg.0 leaves [string], get_Length leaves [int].
// The types come from the importer's stack simulation via the trace hook.
var output = Project(nameof(CfgSampleClass.LengthOf), IlProjectionDepth.Typed);
Assert.Contains("// [string]", output);
Assert.Contains("// [int]", output);
}

[Fact]
public void Structured_LabelsBranchBlocks()
{
Expand Down
44 changes: 37 additions & 7 deletions src/ILInspector.Decompiler/Pipeline/Ir/IlProjection.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Text;
Expand All @@ -11,6 +12,9 @@ public enum IlProjectionDepth
/// <summary>Flat instruction list with resolved operands.</summary>
Raw,

/// <summary>Each instruction annotated with the evaluation-stack types after it (from the importer's stack simulation).</summary>
Typed,

/// <summary>Block-structured output with labels, indentation, and exception-region markers.</summary>
Structured,
}
Expand All @@ -24,10 +28,10 @@ public enum IlProjectionDepth
/// and block structure reuses the importer's <c>FindLeaders</c>, so the views
/// share one analysis with the IR import rather than a parallel one.
///
/// The per-instruction stack-typed depth (the old <c>Typed</c> view) is added
/// separately: it sources stack types from the importer's typed operand stack
/// and so rides on a per-instruction trace, not on this token-and-structure
/// projection.
/// The <see cref="IlProjectionDepth.Typed"/> depth annotates each instruction
/// with the evaluation-stack types after it, sourced from the importer's own
/// stack simulation via an optional per-instruction trace — one analysis shared
/// with the IR import, not a second simulator.
///
/// Exception-safe by construction: any malformed-metadata read or resolver
/// failure surfaces as a diagnosed <see cref="DecompilerResult"/>, never a throw.
Expand All @@ -47,9 +51,35 @@ static string Render(MetadataSource source, string type, string method, IlProjec
var imported = MethodImporter.Import(source, (TypeDefinitionHandle)methodDef.GetDeclaringType(), methodHandle);
var scope = IrImporter.CallerScope(source.Reader, typeDef, methodDef);
var instructions = Decode(source.Reader, scope, imported.Body.IL.AsSpan());
return depth == IlProjectionDepth.Structured
? RenderStructured(instructions, imported.Body)
: RenderRaw(instructions);
return depth switch
{
IlProjectionDepth.Structured => RenderStructured(instructions, imported.Body),
IlProjectionDepth.Typed => RenderTyped(source, imported, scope, instructions),
_ => RenderRaw(instructions),
};
}

static string RenderTyped(MetadataSource source, ImportedMethod imported, GenericScope scope, List<Instr> instructions)
{
// Re-run the importer with a trace: its single stack simulation yields the
// post-instruction stack types, keyed by offset. The instruction text and
// resolved operands still come from Decode, so there is no second decode —
// and no second simulation beyond the import the pipeline already runs.
var trace = new List<IlTracePoint>();
IrImporter.Build(source, imported, scope, trace);
var typesByOffset = new Dictionary<int, ImmutableArray<TypeRef?>>();
foreach (var point in trace)
typesByOffset[point.Offset] = point.StackTypes;

var sb = new StringBuilder();
foreach (var i in instructions)
{
string types = typesByOffset.TryGetValue(i.Offset, out var stack)
? $" // [{string.Join(", ", stack.Select(t => t?.ToDisplayString() ?? "?"))}]"
: "";
sb.AppendLine(Format(i) + types);
}
return sb.ToString();
}

static (TypeDefinition Type, MethodDefinition Method, MethodDefinitionHandle Handle) Locate(
Expand Down
18 changes: 15 additions & 3 deletions src/ILInspector.Decompiler/Pipeline/Ir/IrImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

namespace ILInspector.Decompiler.Pipeline;

/// <summary>
/// A per-instruction snapshot of the evaluation stack (each element's result
/// type) immediately after an opcode is imported — the typed-IL projection's
/// data, captured as a byproduct of the importer's single stack simulation.
/// </summary>
internal readonly record struct IlTracePoint(int Offset, ImmutableArray<TypeRef?> StackTypes);

/// <summary>
/// Builds the typed IR for one method while its <see cref="MetadataSource"/>
/// is alive; the resulting <see cref="IrFunction"/> is fully materialized.
Expand Down Expand Up @@ -128,7 +135,7 @@ internal static GenericScope CallerScope(MetadataReader reader, TypeDefinition t
GenericParameterNames(reader, method.GetGenericParameters()));

/// <summary>Internal for tests: synthetic IL can exercise join shapes C# never compiles to.</summary>
internal static IrFunction Build(MetadataSource source, ImportedMethod method, GenericScope callerScope)
internal static IrFunction Build(MetadataSource source, ImportedMethod method, GenericScope callerScope, List<IlTracePoint>? trace = null)
{
var container = new BlockContainer();
var function = new IrFunction(method.Name, method.DeclaringType, method.Signature, method.Body.Locals, container)
Expand All @@ -154,7 +161,7 @@ internal static IrFunction Build(MetadataSource source, ImportedMethod method, G
{
var block = new Block(leader);
container.Add(block);
if (!BuildBlock(source, method, function, block, span, leader, NextLeader(leaders, leader, span.Length), callerScope, state))
if (!BuildBlock(source, method, function, block, span, leader, NextLeader(leaders, leader, span.Length), callerScope, state, trace))
return function; // honest stop already recorded
}

Expand Down Expand Up @@ -406,7 +413,7 @@ static bool PropagateAndSpill(MetadataSource source, IrFunction function, Block

/// <summary>Builds one block. Returns false when the import stopped honestly inside it.</summary>
static bool BuildBlock(MetadataSource source, ImportedMethod method, IrFunction function, Block body,
ReadOnlySpan<byte> il, int start, int end, GenericScope callerScope, BuildState state)
ReadOnlySpan<byte> il, int start, int end, GenericScope callerScope, BuildState state, List<IlTracePoint>? trace = null)
{
var stack = new Stack<IrExpression>();
var reader = new ILReader(il[..end], currentOffset: start);
Expand Down Expand Up @@ -995,6 +1002,11 @@ or ILOpCode.Conv_ovf_u1_un or ILOpCode.Conv_ovf_u2_un or ILOpCode.Conv_ovf_u4_un
return false;
}

// Capture the post-opcode evaluation-stack types for the typed-IL
// projection — a no-op unless a trace is requested. Prefix opcodes
// `continue` above, so they are correctly excluded.
trace?.Add(new IlTracePoint(offset, [.. stack.Reverse().Select(e => e.ResultType)]));

if (constrainedTo is not null || volatilePrefix || readonlyPrefix)
{
Stop(function, body, stack, offset, opcode.ToString().ToLowerInvariant(),
Expand Down
Loading