Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/fsharp/docs/release-notes/.FSharp.Core/10.0.200.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

* Fix IL2091 trimming warning in `LazyExtensions.Create` by adding `DynamicallyAccessedMembers` attribute to the generic type parameter. ([Issue #17356](https://github.com/dotnet/fsharp/issues/17356), [PR #18302](https://github.com/dotnet/fsharp/pull/18302))
3 changes: 2 additions & 1 deletion src/fsharp/src/FSharp.Core/prim-types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7299,11 +7299,12 @@ namespace Microsoft.FSharp.Core
namespace Microsoft.FSharp.Control

open System
open System.Diagnostics.CodeAnalysis
open Microsoft.FSharp.Core
open Microsoft.FSharp.Core.Operators

module LazyExtensions =
type Lazy<'T> with
type Lazy<[<DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)>]'T> with
[<CompiledName("Create")>] // give the extension member a 'nice', unmangled compiled name, unique within this module
static member Create(creator : unit -> 'T) : Lazy<'T> =
let creator = Func<'T>(creator)
Expand Down
3 changes: 2 additions & 1 deletion src/fsharp/src/FSharp.Core/prim-types.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -6144,6 +6144,7 @@ namespace Microsoft.FSharp.Core

namespace Microsoft.FSharp.Control

open System.Diagnostics.CodeAnalysis
open Microsoft.FSharp.Core

/// <summary>Extensions related to Lazy values.</summary>
Expand All @@ -6152,7 +6153,7 @@ namespace Microsoft.FSharp.Control
[<AutoOpen>]
module LazyExtensions =

type System.Lazy<'T> with
type System.Lazy<[<DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)>]'T> with

/// <summary>Creates a lazy computation that evaluates to the result of the given function when forced.</summary>
///
Expand Down
16 changes: 16 additions & 0 deletions src/fsharp/tests/AheadOfTime/Trimming/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9365,6 +9365,20 @@ module NonStructuralComparisonOverTimeSpanDirect =
do check "test9407" (NonStructuralComparison.hash 11L) (Operators.hash 11L)
do check "test9408" (NonStructuralComparison.hash 11UL) (Operators.hash 11UL)

let testLazySimple () =
let f23 () = let z = lazy (12345) in z.Force()
check "lazy_simple" 12345 (f23())

let testSeqWithTryFinally () =
let result =
seq {
try
yield 1
yield 2
with
| ex -> ()
} |> Seq.toList
check "seq_try_with" [1; 2] result

[<EntryPoint>]
let main _ =
Expand All @@ -9385,6 +9399,8 @@ let main _ =
PercentAPublicTests.tests ()
PercentAInternalTests.tests ()
ClassWithEvents.testWithEventClass ()
testLazySimple ()
testSeqWithTryFinally ()

match !failures with
| [] ->
Expand Down
52 changes: 40 additions & 12 deletions src/fsharp/tests/AheadOfTime/Trimming/check.ps1
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
function CheckTrim($root, $tfm, $outputfile, $expected_len) {
function CheckTrim($root, $tfm, $outputfile, $expected_len, $callerLineNumber) {
Write-Host "Publish and Execute: ${tfm} - ${root}"
Write-Host "Expecting ${expected_len} for ${outputfile}"

$errors = @()
$scriptFile = $PSCommandPath

$cwd = Get-Location
Set-Location (Join-Path $PSScriptRoot "${root}")
$build_output = dotnet publish -restore -c release -f:$tfm "${root}.fsproj" -bl:"../../../../artifacts/log/Release/AheadOfTime/Trimming/${root}_${tfm}.binlog"
Set-Location ${cwd}
if ($LASTEXITCODE -ne 0)
{
Write-Error "Build failed with exit code ${LASTEXITCODE}"
Write-Error "${build_output}" -ErrorAction Stop
$errors += "Build failed with exit code ${LASTEXITCODE}"
Write-Host "##vso[task.logissue type=error;sourcepath=${scriptFile};linenumber=${callerLineNumber}]Build failed for ${root} with exit code ${LASTEXITCODE}"
return $errors
}

$process = Start-Process -FilePath $(Join-Path $PSScriptRoot "${root}\bin\release\${tfm}\win-x64\publish\${root}.exe") -Wait -NoNewWindow -PassThru -RedirectStandardOutput $(Join-Path $PSScriptRoot "output.txt")

# Checking that the test passed
$output = Get-Content $(Join-Path $PSScriptRoot output.txt)
$expected = "All tests passed"
if ($LASTEXITCODE -ne 0)
if ($process.ExitCode -ne 0)
{
Write-Error "Test failed with exit code ${LASTEXITCODE}" -ErrorAction Stop
$errors += "Test failed with exit code $($process.ExitCode)"
Write-Host "##vso[task.logissue type=error;sourcepath=${scriptFile};linenumber=${callerLineNumber}]Test execution failed for ${root} with exit code $($process.ExitCode)"
}
if ($output -eq $expected)
elseif ($output -ne $expected)
{
Write-Host "Test passed"
$errors += "Test failed with unexpected output: Expected '${expected}', Actual '${output}'"
Write-Host "##vso[task.logissue type=error;sourcepath=${scriptFile};linenumber=${callerLineNumber}]Test failed for ${root} with unexpected output: Expected '${expected}', Actual '${output}'"
}
else
{
Write-Error "Test failed with unexpected output:`nExpected:`n`t${expected}`nActual`n`t${output}" -ErrorAction Stop
Write-Host "Test passed"
}

# Checking that the trimmed outputfile binary is of expected size (needs adjustments if test is updated).
Expand All @@ -39,24 +45,46 @@ function CheckTrim($root, $tfm, $outputfile, $expected_len) {
}
elseif ($file_len -ne $expected_len)
{
Write-Error "Test failed with unexpected ${tfm} - trimmed ${outputfile} length:`nExpected:`n`t${expected_len} Bytes`nActual:`n`t${file_len} Bytes`nEither codegen or trimming logic have changed. Please investigate and update expected dll size or report an issue." -ErrorAction Stop
$errors += "Test failed with unexpected ${tfm} - trimmed ${outputfile} length: Expected ${expected_len} Bytes, Actual ${file_len} Bytes"
Write-Host "##vso[task.logissue type=error;sourcepath=${scriptFile};linenumber=${callerLineNumber}]Trimmed ${outputfile} size mismatch for ${root}: Expected ${expected_len} Bytes, Actual ${file_len} Bytes. Either codegen or trimming logic have changed. Please investigate and update expected dll size or report an issue."
}

$fileBeforePublish = Get-Item (Join-Path $PSScriptRoot "${root}\bin\release\${tfm}\win-x64\${outputfile}")
$sizeBeforePublish = $fileBeforePublish.Length
$sizeDiff = $sizeBeforePublish - $file_len
Write-Host "Size of ${tfm} - ${outputfile} before publish: ${sizeBeforePublish} Bytes, which means the diff is ${sizeDiff} Bytes"

return $errors
}

# NOTE: Trimming now errors out on desktop TFMs, as shown below:
# error NETSDK1124: Trimming assemblies requires .NET Core 3.0 or higher.

$allErrors = @()

# Check net9.0 trimmed assemblies
CheckTrim -root "SelfContained_Trimming_Test" -tfm "net9.0" -outputfile "FSharp.Core.dll" -expected_len 300032
$allErrors += CheckTrim -root "SelfContained_Trimming_Test" -tfm "net9.0" -outputfile "FSharp.Core.dll" -expected_len 311296 -callerLineNumber 66

# Check net9.0 trimmed assemblies with static linked FSharpCore
CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net9.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 9154048
$allErrors += CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net9.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 9169408 -callerLineNumber 69

# Check net9.0 trimmed assemblies with F# metadata resources removed
CheckTrim -root "FSharpMetadataResource_Trimming_Test" -tfm "net9.0" -outputfile "FSharpMetadataResource_Trimming_Test.dll" -expected_len 7607296
$allErrors += CheckTrim -root "FSharpMetadataResource_Trimming_Test" -tfm "net9.0" -outputfile "FSharpMetadataResource_Trimming_Test.dll" -expected_len 7609344 -callerLineNumber 72

# Report all errors and exit with failure if any occurred
if ($allErrors.Count -gt 0) {
Write-Host ""
Write-Host "============================================"
Write-Host "TRIMMING TESTS FAILED"
Write-Host "============================================"
Write-Host "Total errors: $($allErrors.Count)"
foreach ($err in $allErrors) {
Write-Error $err
}
exit 1
}

Write-Host ""
Write-Host "============================================"
Write-Host "ALL TRIMMING TESTS PASSED"
Write-Host "============================================"
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="$(SystemDiagnosticsDiagnosticSourceVersion)" />
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);NU1510</NoWarn> <!-- NU1510: Project is explicitly referencing the runtime assembly 'System.Collections.Immutable', however, if we remove it, it tries to find it on the wrong path. Also, local NoWarn does not help - This is just me trying to enforce it -->
<NoWarn>$(NoWarn);NU1510;44</NoWarn> <!-- NU1510: Project is explicitly referencing the runtime assembly 'System.Collections.Immutable', however, if we remove it, it tries to find it on the wrong path. Also, local NoWarn does not help - This is just me trying to enforce it -->
<!-- 44: AssemblyName.CodeBase is deprecated but needed for assembly resolution in VS integration tests -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Collections.Immutable" Version="$(SystemCollectionsImmutableVersion)" GeneratePathProperty="true" />
Expand Down
52 changes: 52 additions & 0 deletions src/fsharp/tests/FSharp.Test.Utilities/VSInstallDiscovery.fs
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,55 @@ module VSInstallDiscovery =
| NotFound reason ->
logAction $"Visual Studio installation not found: {reason}"
None

/// Gets the VS installation directory or fails with a detailed error message.
/// This is the recommended method for test scenarios that require VS to be installed.
let getVSInstallDirOrFail () : string =
match tryFindVSInstallation () with
| Found (path, _) -> path
| NotFound reason ->
failwith $"Visual Studio installation not found: {reason}. Ensure VS is installed or environment variables (VSAPPIDDIR, VS*COMNTOOLS) are set."

/// Assembly resolver for Visual Studio test infrastructure.
/// Provides centralized assembly resolution for VS integration tests.
module VSAssemblyResolver =
open System
open System.IO
open System.Reflection
open System.Globalization

/// Adds an assembly resolver that probes Visual Studio installation directories.
/// This should be called early in test initialization to ensure VS assemblies can be loaded.
let addResolver () =
let vsInstallDir = VSInstallDiscovery.getVSInstallDirOrFail ()

let probingPaths =
[|
Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\Editor")
Path.Combine(vsInstallDir, @"IDE\PublicAssemblies")
Path.Combine(vsInstallDir, @"IDE\PrivateAssemblies")
Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\ManagedLanguages\VBCSharp\LanguageServices")
Path.Combine(vsInstallDir, @"IDE\Extensions\Microsoft\CodeSense\Framework")
Path.Combine(vsInstallDir, @"IDE")
|]

AppDomain.CurrentDomain.add_AssemblyResolve(fun _ args ->
let found () =
probingPaths
|> Seq.tryPick (fun p ->
try
let name = AssemblyName(args.Name)
let codebase = Path.GetFullPath(Path.Combine(p, name.Name) + ".dll")
if File.Exists(codebase) then
name.CodeBase <- codebase
name.CultureInfo <- Unchecked.defaultof<CultureInfo>
name.Version <- Unchecked.defaultof<Version>
Some name
else
None
with _ ->
None)

match found () with
| None -> Unchecked.defaultof<Assembly>
| Some name -> Assembly.Load(name))
2 changes: 2 additions & 0 deletions src/fsharp/vsintegration/Vsix/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<VisualStudioInsertionComponent>Microsoft.FSharp</VisualStudioInsertionComponent>
<VSRootSuffix>RoslynDev</VSRootSuffix>
<VSSDKTargetPlatformRegRootSuffix>$(VSRootSuffix)</VSSDKTargetPlatformRegRootSuffix>
<!-- Path to the locally built F# compiler for use during VSIX debugging -->
<FSharpCompilerPathForDebuggingLocally>$(ArtifactsDir)bin\fscAnyCpu\$(Configuration)\net472\</FSharpCompilerPathForDebuggingLocally>
<CreateVsixContainer>true</CreateVsixContainer>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@
"VisualFSharpDebug": {
"commandName": "Executable",
"executablePath": "$(DevEnvDir)devenv.exe",
"commandLineArgs": "/rootsuffix $(VSSDKTargetPlatformRegRootSuffix) /log"
"commandLineArgs": "/rootsuffix $(VSSDKTargetPlatformRegRootSuffix) /log",
"environmentVariables": {
"FSharpCompilerPath": "$(FSharpCompilerPathForDebuggingLocally)",
"DisableAutoSetFscCompilerPath": "true",
"FSharpPreferNetFrameworkTools": "true",
"FSharp_Shim_Present": "true",
"FSharpPreferAnyCpuTools": "true",
"Fsc_NetFramework_ToolPath": "$(FSharpCompilerPathForDebuggingLocally)",
"Fsc_NetFramework_AnyCpu_ToolExe": "fscAnyCpu.exe"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,9 @@

namespace FSharp.Editor.Tests.Helpers

open System
open System.IO
open System.Reflection

module AssemblyResolver =
open System.Globalization
open FSharp.Test.VSInstallDiscovery

let vsInstallDir =
// Use centralized VS installation discovery with graceful fallback
match tryGetVSInstallDir () with
| Some dir -> dir
| None ->
// Fallback to legacy behavior for backward compatibility
let vsvar =
let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")

if String.IsNullOrEmpty var then
Environment.GetEnvironmentVariable("VSAPPIDDIR")
else
var

if String.IsNullOrEmpty vsvar then
failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."

Path.Combine(vsvar, "..")

let probingPaths =
[|
Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\Editor")
Path.Combine(vsInstallDir, @"IDE\PublicAssemblies")
Path.Combine(vsInstallDir, @"IDE\PrivateAssemblies")
Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\ManagedLanguages\VBCSharp\LanguageServices")
Path.Combine(vsInstallDir, @"IDE\Extensions\Microsoft\CodeSense\Framework")
Path.Combine(vsInstallDir, @"IDE")
|]

let addResolver () =
AppDomain.CurrentDomain.add_AssemblyResolve (fun h args ->
let found () =
(probingPaths)
|> Seq.tryPick (fun p ->
try
let name = AssemblyName(args.Name)
let codebase = Path.GetFullPath(Path.Combine(p, name.Name) + ".dll")

if File.Exists(codebase) then
name.CodeBase <- codebase
name.CultureInfo <- Unchecked.defaultof<CultureInfo>
name.Version <- Unchecked.defaultof<Version>
Some(name)
else
None
with _ ->
None)
open FSharp.Test.VSAssemblyResolver

match found () with
| None -> Unchecked.defaultof<Assembly>
| Some name -> Assembly.Load(name))
/// Adds an assembly resolver that probes Visual Studio installation directories.
/// This is a compatibility shim that delegates to the centralized implementation.
let addResolver = addResolver
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
<Compile Include="..\unittests\TestLib.Utils.fs">
<Link>UnitTests.TestLib.Utils.fs</Link>
</Compile>
<Compile Include="..\..\..\tests\FSharp.Test.Utilities\VSInstallDiscovery.fs">
<Link>VSInstallDiscovery.fs</Link>
</Compile>
<Compile Include="FSharpLanguageServiceTestable.fs" />
<Compile Include="VsMocks.fs" />
<Compile Include="Salsa.fs" />
Expand Down
18 changes: 2 additions & 16 deletions src/fsharp/vsintegration/tests/Salsa/VsMocks.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1642,6 +1642,7 @@ module internal VsActual =
open System.ComponentModel.Composition.Primitives
open Microsoft.VisualStudio.Text
open Microsoft.VisualStudio.Threading
open FSharp.Test.VSInstallDiscovery

type TestExportJoinableTaskContext () =

Expand All @@ -1650,22 +1651,7 @@ module internal VsActual =
[<System.ComponentModel.Composition.Export(typeof<JoinableTaskContext>)>]
member public _.JoinableTaskContext : JoinableTaskContext = jtc

let vsInstallDir =
// use the environment variable to find the VS installdir
let vsvar =
// Try VS180COMNTOOLS first, then VS170COMNTOOLS, then VSAPPIDDIR
// TODO : use tryGetVSInstallDir from test utils instead
let var18 = Environment.GetEnvironmentVariable("VS180COMNTOOLS")
if String.IsNullOrEmpty var18 then
let var17 = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
if String.IsNullOrEmpty var17 then
Environment.GetEnvironmentVariable("VSAPPIDDIR")
else
var17
else
var18
if String.IsNullOrEmpty vsvar then failwith "VS180COMNTOOLS, VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
Path.Combine(vsvar, "..")
let vsInstallDir = getVSInstallDirOrFail ()

let CreateEditorCatalog() =
let thisAssembly = Assembly.GetExecutingAssembly().Location
Expand Down
Loading