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..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,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), [PR #19921](https://github.com/dotnet/fsharp/pull/19921)) ### Added 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() ] 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)