From e0aee1baa5509c62849fc2ae14bab27bcd99daad Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Thu, 30 Jul 2026 10:08:36 +0200 Subject: [PATCH] Fix #3714: declare a local function in its innermost capture scope A local function nested in a lambda can capture closures at two depths: csc emits it as an instance method on the enclosing method's display class that takes the lambda's display class as a parameter. Combining those two capture scopes with FindCommonAncestorInstruction picked the enclosing method, moving the function out of the lambda that owns the deeper closure; the variables captured there were then unreachable and the display-class parameter survived into the output as an undeclared identifier. Nested capture scopes resolve to the innermost instead, which a local function can always see - it reaches the outer closure through the display class it is declared on. Assisted-by: Claude:claude-fable-5:Claude Code --- .../TestCases/Pretty/LocalFunctions.cs | 25 +++++++++++++++++++ .../IL/Transforms/LocalFunctionDecompiler.cs | 15 ++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs index a2278c99e..d16598e09 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs @@ -877,5 +877,30 @@ namespace LocalFunctions static extern int EnumWindows(long hWnd, long lParam); } #endif + + public int Issue3714_LocalFunctionInsideLambda() + { + int outer = 1; +#if !OPT + Action action = () => { +#else + ((Action)(() => { +#endif + int inner = 2; + Local(3); + Console.WriteLine(inner); + void Local(int d) + { + inner += d; + outer += d; + } +#if !OPT + }; + action(); +#else + }))(); +#endif + return outer; + } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs b/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs index 07ce8ce3a..aeb82eff8 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs @@ -732,8 +732,21 @@ namespace ICSharpCode.Decompiler.IL.Transforms } if (function.DeclarationScope == null) function.DeclarationScope = closureVar.CaptureScope; - else if (!IsInNestedLocalFunction(function.DeclarationScope, closureVar.CaptureScope.Ancestors.OfType().First())) + else if (closureVar.CaptureScope.IsDescendantOf(function.DeclarationScope)) + { + // The closures captured by one local function are nested in one another: a + // local function declared inside a lambda still reaches the enclosing method's + // closure, but not the other way round. Where one capture scope contains the + // other, the declaration belongs in the inner one; taking the common ancestor + // would move the function out of the lambda owning the deeper closure and + // leave the variables captured there out of scope. + function.DeclarationScope = closureVar.CaptureScope; + } + else if (!function.DeclarationScope.IsDescendantOf(closureVar.CaptureScope) + && !IsInNestedLocalFunction(function.DeclarationScope, closureVar.CaptureScope.Ancestors.OfType().First())) + { function.DeclarationScope = FindCommonAncestorInstruction(function.DeclarationScope, closureVar.CaptureScope); + } return true; ILInstruction GetClosureInitializer(ILVariable variable)