diff --git a/src/ILInspector.Decompiler.Tests/IlProjectionTests.cs b/src/ILInspector.Decompiler.Tests/IlProjectionTests.cs
index 61f00044..96e49049 100644
--- a/src/ILInspector.Decompiler.Tests/IlProjectionTests.cs
+++ b/src/ILInspector.Decompiler.Tests/IlProjectionTests.cs
@@ -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()
{
diff --git a/src/ILInspector.Decompiler/Pipeline/Ir/IlProjection.cs b/src/ILInspector.Decompiler/Pipeline/Ir/IlProjection.cs
index b5d2f675..74258dcf 100644
--- a/src/ILInspector.Decompiler/Pipeline/Ir/IlProjection.cs
+++ b/src/ILInspector.Decompiler/Pipeline/Ir/IlProjection.cs
@@ -1,4 +1,5 @@
using System.Buffers.Binary;
+using System.Collections.Immutable;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Text;
@@ -11,6 +12,9 @@ public enum IlProjectionDepth
/// Flat instruction list with resolved operands.
Raw,
+ /// Each instruction annotated with the evaluation-stack types after it (from the importer's stack simulation).
+ Typed,
+
/// Block-structured output with labels, indentation, and exception-region markers.
Structured,
}
@@ -24,10 +28,10 @@ public enum IlProjectionDepth
/// and block structure reuses the importer's FindLeaders, so the views
/// share one analysis with the IR import rather than a parallel one.
///
-/// The per-instruction stack-typed depth (the old Typed 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 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 , never a throw.
@@ -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 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();
+ IrImporter.Build(source, imported, scope, trace);
+ var typesByOffset = new Dictionary>();
+ 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(
diff --git a/src/ILInspector.Decompiler/Pipeline/Ir/IrImporter.cs b/src/ILInspector.Decompiler/Pipeline/Ir/IrImporter.cs
index 7edd9640..49dd2c01 100644
--- a/src/ILInspector.Decompiler/Pipeline/Ir/IrImporter.cs
+++ b/src/ILInspector.Decompiler/Pipeline/Ir/IrImporter.cs
@@ -4,6 +4,13 @@
namespace ILInspector.Decompiler.Pipeline;
+///
+/// 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.
+///
+internal readonly record struct IlTracePoint(int Offset, ImmutableArray StackTypes);
+
///
/// Builds the typed IR for one method while its
/// is alive; the resulting is fully materialized.
@@ -128,7 +135,7 @@ internal static GenericScope CallerScope(MetadataReader reader, TypeDefinition t
GenericParameterNames(reader, method.GetGenericParameters()));
/// Internal for tests: synthetic IL can exercise join shapes C# never compiles to.
- internal static IrFunction Build(MetadataSource source, ImportedMethod method, GenericScope callerScope)
+ internal static IrFunction Build(MetadataSource source, ImportedMethod method, GenericScope callerScope, List? trace = null)
{
var container = new BlockContainer();
var function = new IrFunction(method.Name, method.DeclaringType, method.Signature, method.Body.Locals, container)
@@ -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
}
@@ -406,7 +413,7 @@ static bool PropagateAndSpill(MetadataSource source, IrFunction function, Block
/// Builds one block. Returns false when the import stopped honestly inside it.
static bool BuildBlock(MetadataSource source, ImportedMethod method, IrFunction function, Block body,
- ReadOnlySpan il, int start, int end, GenericScope callerScope, BuildState state)
+ ReadOnlySpan il, int start, int end, GenericScope callerScope, BuildState state, List? trace = null)
{
var stack = new Stack();
var reader = new ILReader(il[..end], currentOffset: start);
@@ -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(),