Skip to content
Closed
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
2 changes: 2 additions & 0 deletions docs/release-notes/.FSharp.Compiler.Service/9.0.300.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
* Fix checking bug in unpickling [PR #18430](https://github.com/dotnet/fsharp/pull/18430)
* Reenable β-reduction and subsequent reoptimization of immediately-invoked F#-defined generic delegates. ([PR #18401](https://github.com/dotnet/fsharp/pull/18401))
* Fixed [#18433](https://github.com/dotnet/fsharp/issues/18433), a rare case of an internal error in xml comment processing. ([PR #18436](https://github.com/dotnet/fsharp/pull/18436))
* Fixed [#18441](https://github.com/dotnet/fsharp/issues/18441), Unique names of generated assemblies for each fsi session ([PR #18443](https://github.com/dotnet/fsharp/pull/18443))
* Fix missing `null` highlighting in tooltips ([PR #18457](https://github.com/dotnet/fsharp/pull/18457))


### Added
* Added missing type constraints in FCS. ([PR #18241](https://github.com/dotnet/fsharp/pull/18241))
* The 'use' keyword can be used on IDisposable|null without nullness warnings ([PR #18262](https://github.com/dotnet/fsharp/pull/18262))
Expand Down
219 changes: 114 additions & 105 deletions src/Compiler/Interactive/fsi.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1673,7 +1673,16 @@ let internal mkBoundValueTypedImpl tcGlobals m moduleName name ty =
let qname = QualifiedNameOfFile.QualifiedNameOfFile(Ident(moduleName, m))
entity, v, CheckedImplFile.CheckedImplFile(qname, [], mty, contents, false, false, StampMap.Empty, Map.empty)

let dynamicCcuName = "FSI-ASSEMBLY"
let mkUniqueCcuName =
let mutable ccuId = 0

fun () ->
let nextId = Interlocked.Increment &ccuId

if nextId = 1 then
"FSI-ASSEMBLY"
else
$"FSI-ASSEMBLY{nextId}"

/// Encapsulates the coordination of the typechecking, optimization and code generation
/// components of the F# compiler for interactively executed fragments of code.
Expand Down Expand Up @@ -1718,6 +1727,8 @@ type internal FsiDynamicCompiler

let valuePrinter = FsiValuePrinter(fsi, outWriter)

let dynamicCcuName = mkUniqueCcuName ()

let builders =
if tcConfigB.fsiMultiAssemblyEmit then
None
Expand Down Expand Up @@ -2415,15 +2426,17 @@ type internal FsiDynamicCompiler

member _.DynamicAssemblies = dynamicAssemblies.ToArray()

member _.FindDynamicAssembly(name, useFullName: bool) =
let getName (assemblyName: AssemblyName) : string MaybeNull =
if useFullName then
assemblyName.FullName
else
assemblyName.Name

dynamicAssemblies
|> ResizeArray.tryFind (fun asm -> getName (asm.GetName()) = name)
member _.FindDynamicAssembly(assemblyName: AssemblyName) =
// All dynamic assemblies share the same simple name and differ only in version.
if assemblyName.Name <> dynamicCcuName then
None
elif isNull assemblyName.Version then
// If no specific version is requested, try to return the most recent assembly.
dynamicAssemblies |> Seq.tryLast
else
// Otherwise, try to find an exact match.
dynamicAssemblies
|> ResizeArray.tryFind (fun asm -> asm.FullName = assemblyName.FullName)

member _.EvalParsedSourceFiles(ctok, diagnosticsLogger, istate, inputs, m) =
let prefix = mkFragmentPath m nextFragmentId
Expand Down Expand Up @@ -3274,7 +3287,8 @@ type internal MagicAssemblyResolution() =
try
// Grab the name of the assembly
let tcConfig = TcConfig.Create(tcConfigB, validate = false)
let simpleAssemName = fullAssemName.Split([| ',' |]).[0]
let assemblyName = AssemblyName(fullAssemName)
let simpleAssemName = !!assemblyName.Name

if progress then
fsiConsoleOutput.uprintfn "ATTEMPT MAGIC LOAD ON ASSEMBLY, simpleAssemName = %s" simpleAssemName // "Attempting to load a dynamically required assembly in response to an AssemblyResolve event by using known static assembly references..."
Expand All @@ -3285,129 +3299,124 @@ type internal MagicAssemblyResolution() =
then
null
else
// Check dynamic assemblies by exact version
match fsiDynamicCompiler.FindDynamicAssembly(fullAssemName, true) with
// Check dynamic assemblies
match fsiDynamicCompiler.FindDynamicAssembly(assemblyName) with
| Some asm -> asm
| None ->
// Check dynamic assemblies by simple name
match fsiDynamicCompiler.FindDynamicAssembly(simpleAssemName, false) with
| Some asm when not (tcConfigB.fsiMultiAssemblyEmit) -> asm
| _ ->
// Otherwise continue
let assemblyReferenceTextDll = (simpleAssemName + ".dll")
let assemblyReferenceTextExe = (simpleAssemName + ".exe")

// Otherwise continue
let assemblyReferenceTextDll = (simpleAssemName + ".dll")
let assemblyReferenceTextExe = (simpleAssemName + ".exe")
let overallSearchResult =

let overallSearchResult =
// OK, try to resolve as an existing DLL in the resolved reference set. This does unification by assembly name
// once an assembly has been referenced.
let searchResult =
tcImports.TryFindExistingFullyQualifiedPathBySimpleAssemblyName simpleAssemName

// OK, try to resolve as an existing DLL in the resolved reference set. This does unification by assembly name
// once an assembly has been referenced.
match searchResult with
| Some r -> OkResult([], Choice1Of2 r)
| _ ->

// OK, try to resolve as a .dll
let searchResult =
tcImports.TryFindExistingFullyQualifiedPathBySimpleAssemblyName simpleAssemName
tcImports.TryResolveAssemblyReference(
ctok,
AssemblyReference(m, assemblyReferenceTextDll, None),
ResolveAssemblyReferenceMode.Speculative
)

match searchResult with
| Some r -> OkResult([], Choice1Of2 r)
| OkResult(warns, [ r ]) -> OkResult(warns, Choice1Of2 r.resolvedPath)
| _ ->

// OK, try to resolve as a .dll
// OK, try to resolve as a .exe
let searchResult =
tcImports.TryResolveAssemblyReference(
ctok,
AssemblyReference(m, assemblyReferenceTextDll, None),
AssemblyReference(m, assemblyReferenceTextExe, None),
ResolveAssemblyReferenceMode.Speculative
)

match searchResult with
| OkResult(warns, [ r ]) -> OkResult(warns, Choice1Of2 r.resolvedPath)
| _ ->

// OK, try to resolve as a .exe
if progress then
fsiConsoleOutput.uprintfn "ATTEMPT LOAD, assemblyReferenceTextDll = %s" assemblyReferenceTextDll

/// Take a look through the files quoted, perhaps with explicit paths
let searchResult =
tcImports.TryResolveAssemblyReference(
ctok,
AssemblyReference(m, assemblyReferenceTextExe, None),
ResolveAssemblyReferenceMode.Speculative
)
tcConfig.referencedDLLs
|> List.tryPick (fun assemblyReference ->
if progress then
fsiConsoleOutput.uprintfn
"ATTEMPT MAGIC LOAD ON FILE, referencedDLL = %s"
assemblyReference.Text

if
String.Compare(
FileSystemUtils.fileNameOfPath assemblyReference.Text,
assemblyReferenceTextDll,
StringComparison.OrdinalIgnoreCase
) = 0
|| String.Compare(
FileSystemUtils.fileNameOfPath assemblyReference.Text,
assemblyReferenceTextExe,
StringComparison.OrdinalIgnoreCase
) = 0
then
Some(
tcImports.TryResolveAssemblyReference(
ctok,
assemblyReference,
ResolveAssemblyReferenceMode.Speculative
)
)
else
None)

match searchResult with
| OkResult(warns, [ r ]) -> OkResult(warns, Choice1Of2 r.resolvedPath)
| Some(OkResult(warns, [ r ])) -> OkResult(warns, Choice1Of2 r.resolvedPath)
| _ ->

if progress then
fsiConsoleOutput.uprintfn "ATTEMPT LOAD, assemblyReferenceTextDll = %s" assemblyReferenceTextDll

/// Take a look through the files quoted, perhaps with explicit paths
let searchResult =
tcConfig.referencedDLLs
|> List.tryPick (fun assemblyReference ->
if progress then
fsiConsoleOutput.uprintfn
"ATTEMPT MAGIC LOAD ON FILE, referencedDLL = %s"
assemblyReference.Text

if
String.Compare(
FileSystemUtils.fileNameOfPath assemblyReference.Text,
assemblyReferenceTextDll,
StringComparison.OrdinalIgnoreCase
) = 0
|| String.Compare(
FileSystemUtils.fileNameOfPath assemblyReference.Text,
assemblyReferenceTextExe,
StringComparison.OrdinalIgnoreCase
) = 0
then
Some(
tcImports.TryResolveAssemblyReference(
ctok,
assemblyReference,
ResolveAssemblyReferenceMode.Speculative
)
)
else
None)

match searchResult with
| Some(OkResult(warns, [ r ])) -> OkResult(warns, Choice1Of2 r.resolvedPath)
| _ ->

#if !NO_TYPEPROVIDERS
match tcImports.TryFindProviderGeneratedAssemblyByName(ctok, simpleAssemName) with
| Some assembly -> OkResult([], Choice2Of2 assembly)
| None ->
match tcImports.TryFindProviderGeneratedAssemblyByName(ctok, simpleAssemName) with
| Some assembly -> OkResult([], Choice2Of2 assembly)
| None ->
#endif

// As a last resort, try to find the reference without an extension
match
tcImports.TryFindExistingFullyQualifiedPathByExactAssemblyRef(
ILAssemblyRef.Create(simpleAssemName, None, None, false, None, None)
)
with
| Some resolvedPath -> OkResult([], Choice1Of2 resolvedPath)
| None ->

ErrorResult([], Failure(FSIstrings.SR.fsiFailedToResolveAssembly (simpleAssemName)))

match overallSearchResult with
| ErrorResult _ -> null
| OkResult _ ->
let res = CommitOperationResult overallSearchResult

match res with
| Choice1Of2 assemblyName ->
if simpleAssemName <> "Mono.Posix" && progress then
fsiConsoleOutput.uprintfn "%s" (FSIstrings.SR.fsiBindingSessionTo (assemblyName))

if isRunningOnCoreClr then
// As a last resort, try to find the reference without an extension
match
tcImports.TryFindExistingFullyQualifiedPathByExactAssemblyRef(
ILAssemblyRef.Create(simpleAssemName, None, None, false, None, None)
)
with
| Some resolvedPath -> OkResult([], Choice1Of2 resolvedPath)
| None ->

ErrorResult([], Failure(FSIstrings.SR.fsiFailedToResolveAssembly (simpleAssemName)))

match overallSearchResult with
| ErrorResult _ -> null
| OkResult _ ->
let res = CommitOperationResult overallSearchResult

match res with
| Choice1Of2 assemblyName ->
if simpleAssemName <> "Mono.Posix" && progress then
fsiConsoleOutput.uprintfn "%s" (FSIstrings.SR.fsiBindingSessionTo (assemblyName))

if isRunningOnCoreClr then
assemblyLoadFrom assemblyName
else
try
let an = AssemblyName.GetAssemblyName(assemblyName)
an.CodeBase <- assemblyName
Assembly.Load an
with _ ->
assemblyLoadFrom assemblyName
else
try
let an = AssemblyName.GetAssemblyName(assemblyName)
an.CodeBase <- assemblyName
Assembly.Load an
with _ ->
assemblyLoadFrom assemblyName
| Choice2Of2 assembly -> assembly
| Choice2Of2 assembly -> assembly

with e ->
stopProcessingRecovery e range0
Expand Down
40 changes: 34 additions & 6 deletions tests/FSharp.Compiler.ComponentTests/Scripting/Interactive.fs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ module ``Interactive tests`` =
]

[<Theory>]
[<InlineData(true)>]
[<InlineData(false)>]
let ``Evaluation of multiple sessions should succeed`` (useMultiEmit) =
[<InlineData(true, true)>]
[<InlineData(false, false)>]
[<InlineData(false, true)>]
let ``Evaluation of multiple sessions should succeed`` (useMultiEmit1, useMultiEmit2) =

let args : string array = [| if useMultiEmit then "--multiemit+" else "--multiemit-"|]
use sessionOne = new FSharpScript(additionalArgs=args)
use sessionTwo = new FSharpScript(additionalArgs=args)
let args useMultiEmit : string array = [| if useMultiEmit then "--multiemit+" else "--multiemit-"|]
use sessionOne = new FSharpScript(additionalArgs = args useMultiEmit1)
use sessionTwo = new FSharpScript(additionalArgs = args useMultiEmit2)

sessionOne.Eval("""
module Test1 =
Expand All @@ -63,6 +64,33 @@ module Test2 =
Assert.Equal(typeof<string>, value2.ReflectionType)
Assert.Equal("Execute - Test2.test2 - 27", value2.ReflectionValue :?> string)

[<Theory>]
[<InlineData(true)>]
[<InlineData(false)>]
let ``Currently executing dynamic assembly can be resolved by simple name and full name`` useMultiEmit =
let args = [| if useMultiEmit then "--multiemit+" else "--multiemit-" |]
use session = new FSharpScript(additionalArgs = args)
let simpleNameResult = session.Eval("""System.Reflection.Assembly.GetExecutingAssembly().GetName().Name""") |> getValue
let fullNameResult = session.Eval("""System.Reflection.Assembly.GetExecutingAssembly().FullName""") |> getValue
System.Reflection.Assembly.Load(string simpleNameResult.Value.ReflectionValue) |> ignore
System.Reflection.Assembly.Load(string fullNameResult.Value.ReflectionValue) |> ignore

[<Fact>]
let ``Multiple sessions should have unique assembly names`` () =
let args useMultiEmit : string array = [| if useMultiEmit then "--multiemit+" else "--multiemit-"|]
use session1 = new FSharpScript(additionalArgs = args true)
use session2 = new FSharpScript(additionalArgs = args false)
use session3 = new FSharpScript(additionalArgs = args true)
use session4 = new FSharpScript(additionalArgs = args false)

let names =
[ for session in [session1; session2; session3; session4] do
let result = session.Eval("""System.Reflection.Assembly.GetExecutingAssembly().GetName().Name""") |> getValue
result.Value.ReflectionValue |> string ]

printfn "%A" names
Assert.True(names |> List.distinct = names, "Assembly names are not unique across sessions")

module ``External FSI tests`` =
[<Fact>]
let ``Eval object value``() =
Expand Down
Loading