Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions src/Compiler/Interactive/fsi.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
]

Expand Down
88 changes: 88 additions & 0 deletions tests/FSharp.Compiler.ComponentTests/Scripting/Interactive.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<System.Diagnostics.DebuggableAttribute>, 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

[<Fact>]
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}"
)

[<Fact>]
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)

[<Fact>]
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}"
)

[<Fact>]
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(
"""
[<assembly: System.Diagnostics.DebuggableAttribute(System.Diagnostics.DebuggableAttribute.DebuggingModes.Default)>]
do ()
"""
)

Assert.Empty(errors)

match userDecl with
| Result.Ok _ -> ()
| Result.Error ex -> raise ex

let flags = evalDebuggableFlags session

Assert.Equal(1, flags.Length)
Loading