From 037e067843968b2bb74e58a74e25e6cf01f90822 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 9 Jun 2026 12:50:01 +0200 Subject: [PATCH 1/4] tests: add failing manifest-metadata assertions for FSI multi-emit DebuggableAttribute (#14572) Phase 1 (RED) of TDD for issue #14572. Adds four xUnit Facts under Scripting.MultiEmit.DebuggableAttributeManifest that reflect on Assembly.GetExecutingAssembly() inside an FSI submission to inspect the manifest-level DebuggableAttribute that the multi-emit code path is expected to attach when --debug+ is in effect. On current main: * multi-emit submission with --debug+ has DebuggableAttribute with DisableOptimizations -> FAILS (manifest has no DebuggableAttribute) * multi-emit submission with --debug- has no DebuggableAttribute -> passes (negative control) * single-emit submission with --debug+ keeps DebuggableAttribute (regression) -> passes (uses --optimize- to exercise the ilreflect.fs codepath that gates on optimize=false) * multi-emit + --debug+ does not duplicate user-declared DebuggableAttribute -> FAILS (no attribute attached at all yet) The production fix lives in a separate sprint; no source files under src/ are touched here. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scripting/Interactive.fs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/Scripting/Interactive.fs b/tests/FSharp.Compiler.ComponentTests/Scripting/Interactive.fs index 0d597f182e2..5cbce104078 100644 --- a/tests/FSharp.Compiler.ComponentTests/Scripting/Interactive.fs +++ b/tests/FSharp.Compiler.ComponentTests/Scripting/Interactive.fs @@ -252,3 +252,91 @@ match x with """ |> eval |> shouldSucceed + + // https://github.com/dotnet/fsharp/issues/14572 + // Verify that the per-submission dynamic assembly emitted by FSI in --multiemit+ mode + // carries a manifest-level DebuggableAttribute when --debug+ is in effect, so that the + // CLR's JIT does not optimize away locals (which would empty Locals/Autos/Watch in VS). + module DebuggableAttributeManifest = + + let private reflectionHelperScript = + """ +let asm = System.Reflection.Assembly.GetExecutingAssembly() +asm.GetCustomAttributes(typeof, false) +|> Array.map (fun a -> int (a :?> System.Diagnostics.DebuggableAttribute).DebuggingFlags) +""" + + let private evalDebuggableFlags (session: FSharpScript) : int[] = + let result, errors = session.Eval(reflectionHelperScript) + Assert.Empty(errors) + + match result with + | Result.Ok(Some v) -> v.ReflectionValue :?> int[] + | Result.Ok None -> failwith "Expected a value from reflection helper script" + | Result.Error ex -> raise ex + + let private disableOptimizationsBit = + int System.Diagnostics.DebuggableAttribute.DebuggingModes.DisableOptimizations + + [] + let ``multi-emit submission with --debug+ has DebuggableAttribute with DisableOptimizations`` () = + let args: string array = [| "--multiemit+"; "--debug+" |] + use session = new FSharpScript(additionalArgs = args) + let flags = evalDebuggableFlags session + + Assert.NotEmpty(flags) + + Assert.True( + flags |> Array.exists (fun f -> f &&& disableOptimizationsBit <> 0), + $"Expected at least one DebuggableAttribute with DisableOptimizations bit set on the FSI submission's manifest, but got DebuggingFlags = %A{flags}" + ) + + [] + let ``multi-emit submission with --debug- has no DebuggableAttribute`` () = + let args: string array = [| "--multiemit+"; "--debug-" |] + use session = new FSharpScript(additionalArgs = args) + let flags = evalDebuggableFlags session + + Assert.Empty(flags) + + [] + let ``single-emit submission with --debug+ keeps DebuggableAttribute (regression)`` () = + // ilreflect.fs's mkDynamicAssemblyAndModule attaches DebuggableAttribute only when + // local optimizations are disabled. --optimize- gates that codepath, so include it + // here to make this a faithful regression test of the existing single-emit behavior. + let args: string array = [| "--multiemit-"; "--debug+"; "--optimize-" |] + use session = new FSharpScript(additionalArgs = args) + let flags = evalDebuggableFlags session + + Assert.NotEmpty(flags) + + Assert.True( + flags |> Array.exists (fun f -> f &&& disableOptimizationsBit <> 0), + $"Expected at least one DebuggableAttribute with DisableOptimizations bit set on the single-emit FSI submission's manifest, but got DebuggingFlags = %A{flags}" + ) + + [] + let ``multi-emit + --debug+ does not duplicate user-declared DebuggableAttribute`` () = + let args: string array = [| "--multiemit+"; "--debug+" |] + use session = new FSharpScript(additionalArgs = args) + + // User declares the attribute themselves in a prior submission. The fix must + // still emit exactly one DebuggableAttribute on subsequent submissions' manifests + // (i.e. the auto-attach must not introduce a duplicate when one is already present). + let userDecl, errors = + session.Eval( + """ +[] +do () +""" + ) + + Assert.Empty(errors) + + match userDecl with + | Result.Ok _ -> () + | Result.Error ex -> raise ex + + let flags = evalDebuggableFlags session + + Assert.Equal(1, flags.Length) From d8d871bb5ac73b8d32f3f2d6545391fac836c1ff Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 9 Jun 2026 13:14:39 +0200 Subject: [PATCH 2/4] fsi: attach DebuggableAttribute to multi-emit manifests when --debug+ (#14572) When FSI is configured for multi-assembly emit (--multiemit+) the per-submission dynamic assembly's manifest now carries System.Diagnostics.DebuggableAttribute with DisableOptimizations|Default whenever --debug is enabled, matching the single-emit path (ilreflect.fs) and the regular compiler (CreateILModule.fs). Dedups against a user-declared assembly DebuggableAttribute. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Interactive/fsi.fs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Compiler/Interactive/fsi.fs b/src/Compiler/Interactive/fsi.fs index 30801263752..fce2cfd212c 100644 --- a/src/Compiler/Interactive/fsi.fs +++ b/src/Compiler/Interactive/fsi.fs @@ -1876,9 +1876,15 @@ type internal FsiDynamicCompiler let manifest = let manifest = ilxMainModule.Manifest.Value + let hasUserDebuggableAttr = + manifest.CustomAttrs.AsList() + |> List.exists (fun a -> a.Method.DeclaringType.TypeRef.FullName = "System.Diagnostics.DebuggableAttribute") + let attrs = [ tcGlobals.MakeInternalsVisibleToAttribute(dynamicCcuName tcConfigB.fsiMultiAssemblyEmit) + if generateDebugInfo && not hasUserDebuggableAttr then + tcGlobals.mkDebuggableAttributeV2 (tcConfigB.jitTracking, true) yield! manifest.CustomAttrs.AsList() ] From 549a8a43f0d1e91afd8e64494ddd88ebc55a2fdf Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 9 Jun 2026 13:47:32 +0200 Subject: [PATCH 3/4] docs: release notes for FSI multi-emit DebuggableAttribute fix (#14572) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 965957dbc0c..dd183dcf44b 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -75,6 +75,7 @@ ([PR #19724](https://github.com/dotnet/fsharp/pull/19724)) * Emit debug points at a stack-empty position ([PR #19877](https://github.com/dotnet/fsharp/pull/19877)) * Fix spurious XmlDoc warnings (unknown parameter / no documentation for parameter) under `--warnon:3390` when a get/set property documents the full parameter set across both accessors. ([Issue #13684](https://github.com/dotnet/fsharp/issues/13684), [PR #19884](https://github.com/dotnet/fsharp/pull/19884)) +* FSI multi-assembly emit (`--multiemit+`) now attaches `System.Diagnostics.DebuggableAttribute(DisableOptimizations|Default)` to each submission's manifest when `--debug+` is set, matching the single-emit and regular-compiler behavior so debuggers see submissions as unoptimized. ([Issue #14572](https://github.com/dotnet/fsharp/issues/14572)) ### Added From 518a5c7bdfcfc50a2ccac65e1c44d3dd4aeeb0ee Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 9 Jun 2026 14:47:47 +0200 Subject: [PATCH 4/4] docs: add PR link to release notes entry (#14572) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index dd183dcf44b..1fd619535b7 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -75,7 +75,7 @@ ([PR #19724](https://github.com/dotnet/fsharp/pull/19724)) * Emit debug points at a stack-empty position ([PR #19877](https://github.com/dotnet/fsharp/pull/19877)) * Fix spurious XmlDoc warnings (unknown parameter / no documentation for parameter) under `--warnon:3390` when a get/set property documents the full parameter set across both accessors. ([Issue #13684](https://github.com/dotnet/fsharp/issues/13684), [PR #19884](https://github.com/dotnet/fsharp/pull/19884)) -* FSI multi-assembly emit (`--multiemit+`) now attaches `System.Diagnostics.DebuggableAttribute(DisableOptimizations|Default)` to each submission's manifest when `--debug+` is set, matching the single-emit and regular-compiler behavior so debuggers see submissions as unoptimized. ([Issue #14572](https://github.com/dotnet/fsharp/issues/14572)) +* FSI multi-assembly emit (`--multiemit+`) now attaches `System.Diagnostics.DebuggableAttribute(DisableOptimizations|Default)` to each submission's manifest when `--debug+` is set, matching the single-emit and regular-compiler behavior so debuggers see submissions as unoptimized. ([Issue #14572](https://github.com/dotnet/fsharp/issues/14572), [PR #19921](https://github.com/dotnet/fsharp/pull/19921)) ### Added