From 9c1cfc98e78bd6b76a1029a34e5902e6960b8396 Mon Sep 17 00:00:00 2001 From: Rich Lander Date: Thu, 18 Jun 2026 07:05:25 -0700 Subject: [PATCH] Add IlProjection Typed depth via an importer per-instruction trace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes IlProjection's parity with the old AnnotatedILEmitter's three depths. The Typed view annotates each instruction with the evaluation-stack types after it. Rather than a second stack simulator, the importer optionally emits a per-instruction trace (offset -> stack ResultTypes) as a byproduct of the single import it already runs: an optional `List? trace` threaded Build -> BuildBlock, recorded right after each opcode's switch arm (prefix opcodes `continue` past it and are correctly excluded; honest stops `return` before it). Null in the product import path, so that path is byte-identical. IlProjection.Project(..., Typed) runs the traced import for types and reuses Decode for the instruction text + resolved operands — one decode, one simulation. The new view is post-instruction stack state with the pipeline's richer typing (e.g. `bool` for `ceq`), vs the old emitter's pre-instruction state; same information, an improved spelling (the gate is exact-or-better). Tests: +1 (per-instruction stack-type annotation); ILInspector.Decompiler.Tests 441 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../IlProjectionTests.cs | 10 +++++ .../Pipeline/Ir/IlProjection.cs | 44 ++++++++++++++++--- .../Pipeline/Ir/IrImporter.cs | 18 ++++++-- 3 files changed, 62 insertions(+), 10 deletions(-) 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(),