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 @@ -8,6 +8,7 @@
* Fix exponential (2^N) compile time in pattern matching with shared guards and partial active patterns. ([Issue #18425](https://github.com/dotnet/fsharp/issues/18425), [PR #20244](https://github.com/dotnet/fsharp/pull/20244))
* Fix incorrect Debug lowering of inline builders that compose low-level resumable state machines. ([Issue #20466](https://github.com/dotnet/fsharp/issues/20466), [PR #20469](https://github.com/dotnet/fsharp/pull/20469))
* Fix incorrect `StructLayout(Size = 1)` emission for data-less struct unions where the compiler-generated tag field makes the actual runtime size larger. ([PR #19759](https://github.com/dotnet/fsharp/pull/19759))
* Embed F# metadata-removal rules before resource collection, preserve custom rules, and combine generated rules when linking assemblies. ([PR #20527](https://github.com/dotnet/fsharp/pull/20527))
* Fix FS0750 "This construct may only be used within computation expressions" incorrectly raised for `let!`/`use!`/`do!` appearing in the right-hand side of a plain `let` binding inside a computation expression. The right-hand side is now desugared as a nested computation of the same builder whose result is bound with `let!`, keeping its bindings correctly scoped. ([Issue #19457](https://github.com/dotnet/fsharp/issues/19457), [PR #19868](https://github.com/dotnet/fsharp/pull/19868))
* Stop leaking a `System.Diagnostics.Metrics.MeterListener` per `Cache` in DEBUG builds. Each cache created a `CacheMetrics.CacheMetricsListener` (which starts a `MeterListener` registered in the process-global metrics registry) and never disposed it, so listeners accumulated for the lifetime of the process. Because every cache hit/miss/add published to all registered listeners, the per-operation cost grew linearly with the number of leaked listeners, so repeated checks (and Debug FCS test runs) slowed down over time. The per-cache `CacheMetricsListener` and the per-instance `cacheId` tag are removed; `DebugDisplay` and tests now read the existing name-aggregated stats populated by the single `ListenToAll` listener, so no per-cache listener is created and no per-operation cost is added. ([PR #19995](https://github.com/dotnet/fsharp/pull/19995))
* Fix state machine lowering dropping the side-effectful receiver of an unused unit-typed member access (e.g. inside `task { (effectful()).UnitProp }`). ([Issue #13099](https://github.com/dotnet/fsharp/issues/13099), [PR #19885](https://github.com/dotnet/fsharp/pull/19885))
Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/Driver/CompilerImports.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ val IsOptimizationDataResource: ILResource -> bool
/// Determine if an IL resource attached to an F# assembly is an F# optimization data resource (data stream B)
val IsOptimizationDataResourceB: ILResource -> bool

val GetNameOfILModule: ILModuleDef -> string

/// Determine if an IL resource attached to an F# assembly is an F# quotation data resource for reflected definitions
val IsReflectedDefinitionsResource: ILResource -> bool

Expand Down
21 changes: 21 additions & 0 deletions src/Compiler/Driver/ILLinkSubstitutions.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

module internal FSharp.Compiler.ILLinkSubstitutions

open System.Xml.Linq

let names (assemblyName: string) =
seq {
for kind in [ "Signature"; "Optimization" ] do
for suffix in [ "Data"; "CompressedData"; "DataB"; "CompressedDataB"; "Info" ] do
yield $"FSharp{kind}{suffix}.{assemblyName}"
}

let document (assemblyName: string) (names: seq<string>) =
let x = XName.Get
let assembly = XElement(x "assembly", XAttribute(x "fullname", assemblyName))

for name in names do
assembly.Add(XElement(x "resource", XAttribute(x "name", name), XAttribute(x "action", "remove"), ""))

XElement(x "linker", assembly)
119 changes: 77 additions & 42 deletions src/Compiler/Driver/StaticLinking.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
module internal FSharp.Compiler.StaticLinking

open System
open System.Xml.Linq
open Internal.Utilities.Collections
open Internal.Utilities.Library
open Internal.Utilities.Library.Extras
Expand All @@ -23,6 +24,57 @@ open FSharp.Compiler.TypedTree
open FSharp.Compiler.TypeProviders
#endif

let linkMetadataResources assemblyName (resources: (string * ILResource) list) =
let xname = XName.Get
let prefixes = ILLinkSubstitutions.names "" |> Seq.toList

let generated, retained =
resources
|> List.splitChoose (fun (owner, resource) ->
match resource.Location with
| ILResourceLocation.Local _ when resource.Name.Equals("ILLink.Substitutions.xml", StringComparison.OrdinalIgnoreCase) ->
try
use stream = resource.GetBytes().AsStream()
let xml = XElement.Load stream

let names =
[
for rule in xml.Descendants(xname "resource") do
if rule.IsEmpty then
rule.Value <- ""

for name in rule.Attributes(xname "name") do
if List.exists name.Value.StartsWithOrdinal prefixes then
name.Value
]

match resource.CustomAttrs.AsList(), XNode.DeepEquals(xml, ILLinkSubstitutions.document owner names) with
| [], true -> Choice1Of2(resource, names)
| _ -> Choice2Of2(true, resource)
with :? System.Xml.XmlException ->
Choice2Of2(true, resource)
| _ -> Choice2Of2(false, resource))

let resources = List.map snd retained

match generated with
| (resource, _) :: _ when not (List.exists fst retained) ->
let present = resources |> List.map _.Name |> Set.ofList

let names =
generated |> List.collect snd |> List.filter present.Contains |> List.distinct

resources
@ [
if not names.IsEmpty then
let xml = ILLinkSubstitutions.document assemblyName names

{ resource with
Location = ILResourceLocation.Local(ByteStorage.FromByteArray(System.Text.Encoding.UTF8.GetBytes(xml.ToString())))
}
]
| _ -> resources

// Handles TypeForwarding for the generated IL model
type TypeForwarding(tcImports: TcImports) =

Expand Down Expand Up @@ -157,51 +209,30 @@ let StaticLinkILModules
]

let savedResources =
let allResources =
[
for ccu, m in dependentILModules do
for r in m.Resources.AsList() do
(ccu, r)
]
// Don't save interface, optimization or resource definitions for provider-generated assemblies.
// These are "fake".
let isProvided (ccu: CcuThunk option) =
[
for ccu, m in dependentILModules do
let provided =
#if !NO_TYPEPROVIDERS
match ccu with
| Some c -> c.IsProviderGenerated
| None -> false
ccu |> Option.exists _.IsProviderGenerated
#else
ignore ccu
false
ignore ccu
false
#endif
for r in m.Resources.AsList() do
let order, enabled =
if IsSignatureDataResource r || IsSignatureDataResourceB r then
0, tcConfig.GenerateSignatureData
elif IsOptimizationDataResource r || IsOptimizationDataResourceB r then
1, tcConfig.GenerateOptimizationData
else
2, true

// Save only the interface/optimization attributes of generated data
let intfDataResources, others =
allResources
|> List.partition (fun (_, r) -> IsSignatureDataResource r || IsSignatureDataResourceB r)

let intfDataResources =
[
for ccu, r in intfDataResources do
if tcConfig.GenerateSignatureData && not (isProvided ccu) then
r
]

let optDataResources, others =
others
|> List.partition (fun (_, r) -> IsOptimizationDataResource r || IsOptimizationDataResourceB r)

let optDataResources =
[
for ccu, r in optDataResources do
if tcConfig.GenerateOptimizationData && not (isProvided ccu) then
r
]

let otherResources = others |> List.map snd

let result = intfDataResources @ optDataResources @ otherResources
result
if enabled && (order = 2 || not provided) then
yield order, (GetNameOfILModule m, r)
]
|> List.groupBy fst
|> List.sortBy fst
|> List.collect (snd >> List.map snd)

let moduls = ilxMainModule :: (List.map snd dependentILModules)

Expand Down Expand Up @@ -248,7 +279,11 @@ let StaticLinkILModules
]
)
TypeDefs = mkILTypeDefs (topTypeDef :: List.concat normalTypeDefs)
Resources = mkILResources (savedResources @ ilxMainModule.Resources.AsList())
Resources =
savedResources
@ (ilxMainModule.Resources.AsList() |> List.map (fun r -> oldManifest.Name, r))
|> linkMetadataResources oldManifest.Name
|> mkILResources
NativeResources = savedNativeResources
}

Expand Down
1 change: 1 addition & 0 deletions src/Compiler/FSharp.Compiler.Service.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@
<Compile Include="Driver\XmlDocFileWriter.fs" />
<Compile Include="Driver\BinaryResourceFormats.fsi" />
<Compile Include="Driver\BinaryResourceFormats.fs" />
<Compile Include="Driver\ILLinkSubstitutions.fs" />
<Compile Include="Driver\StaticLinking.fsi" />
<Compile Include="Driver\StaticLinking.fs" />
<Compile Include="Driver\CreateILModule.fsi" />
Expand Down
4 changes: 3 additions & 1 deletion src/Compiler/Utilities/FileSystem.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1092,9 +1092,11 @@ type ByteStorage(getByteMemory: unit -> ReadOnlyByteMemory) =
static member FromByteMemory(bytes: ReadOnlyByteMemory) = ByteStorage(fun () -> bytes)

static member FromByteMemoryAndCopy(bytes: ReadOnlyByteMemory, useBackingMemoryMappedFile: bool) =
let length = bytes.Length

if useBackingMemoryMappedFile then
match MemoryMappedFile.TryFromByteMemory(bytes) with
| Some mmf -> ByteStorage(fun () -> ByteMemory.FromMemoryMappedFile(mmf).AsReadOnly())
| Some mmf -> ByteStorage(fun () -> ByteMemory.FromMemoryMappedFile(mmf).Slice(0, length).AsReadOnly())
| _ ->
let copiedBytes = ByteMemory.FromArray(bytes.ToArray()).AsReadOnly()
ByteStorage.FromByteMemory(copiedBytes)
Expand Down
1 change: 1 addition & 0 deletions src/FSharp.Build/FSharp.Build.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<Compile Include="CreateFSharpManifestResourceName.fs" />
<Compile Include="SubstituteText.fs" />
<Compile Include="MapSourceRoots.fs" />
<Compile Include="../Compiler/Driver/ILLinkSubstitutions.fs" />
<Compile Include="GenerateILLinkSubstitutions.fs" />
<None Include="Microsoft.FSharp.Targets" CopyToOutputDirectory="PreserveNewest" />
<None Include="Microsoft.Portable.FSharp.Targets" CopyToOutputDirectory="PreserveNewest" />
Expand Down
57 changes: 10 additions & 47 deletions src/FSharp.Build/GenerateILLinkSubstitutions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

namespace FSharp.Build

open System
open System.IO
open System.Text
open FSharp.Compiler
open Microsoft.Build.Framework
open Microsoft.Build.Utilities

Expand Down Expand Up @@ -34,58 +33,22 @@ type GenerateILLinkSubstitutions() =

override this.Execute() =
try
// Define the resource prefixes that need to be removed
let resourcePrefixes =
[|
// Signature variants
yield!
[|
for dataType in [| "Data"; "DataB" |] do
for compression in [| ""; "Compressed" |] do
yield $"FSharpSignature{compression}{dataType}"
|]
let xmlContent =
ILLinkSubstitutions.document this.AssemblyName (ILLinkSubstitutions.names this.AssemblyName)
|> string

// Optimization variants
yield!
[|
for dataType in [| "Data"; "DataB" |] do
for compression in [| ""; "Compressed" |] do
yield $"FSharpOptimization{compression}{dataType}"
|]

// Info variants
yield "FSharpOptimizationInfo"
yield "FSharpSignatureInfo"
|]

// Generate the XML content
let sb = StringBuilder(4096) // pre-allocate capacity
sb.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>") |> ignore
sb.AppendLine("<linker>") |> ignore
sb.AppendLine($" <assembly fullname=\"{this.AssemblyName}\">") |> ignore

// Add each resource entry with proper closing tag on the same line
for prefix in resourcePrefixes do
sb.AppendLine($" <resource name=\"{prefix}.{this.AssemblyName}\" action=\"remove\"></resource>")
|> ignore

// Close assembly and linker tags
sb.AppendLine(" </assembly>") |> ignore
sb.AppendLine("</linker>") |> ignore

let xmlContent = sb.ToString()

// Create a file in the intermediate output path
let outputFileName =
Path.Combine(this.IntermediateOutputPath, "ILLink.Substitutions.xml")

Directory.CreateDirectory(this.IntermediateOutputPath) |> ignore
File.WriteAllText(outputFileName, xmlContent)

// Create a TaskItem for the generated file
let item = TaskItem(outputFileName) :> ITaskItem
item.SetMetadata("LogicalName", "ILLink.Substitutions.xml")
if not (File.Exists outputFileName && File.ReadAllText(outputFileName) = xmlContent) then
File.WriteAllText(outputFileName, xmlContent)

let item = TaskItem(outputFileName.Replace("%", "%25")) :> ITaskItem
item.SetMetadata("LogicalName", "ILLink.Substitutions.xml")
item.SetMetadata("Type", "Non-Resx")
item.SetMetadata("WithCulture", "false")
this.GeneratedItems <- [| item |]
true
with ex ->
Expand Down
5 changes: 4 additions & 1 deletion src/FSharp.Build/Microsoft.FSharp.NetSdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,10 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and
<UsingTask TaskName="GenerateILLinkSubstitutions" AssemblyFile="$(FSharpBuildAssemblyFile)" />

<!-- Generate ILLink.Substitutions.xml to remove F# metadata resources during trimming. F# Core already has the resource defined -->
<Target Name="GenerateFSharpILLinkSubstitutions" BeforeTargets="CoreCompile" Condition="'$(DisableILLinkSubstitutions)' != 'true' and '$(AssemblyName)' != 'FSharp.Core'">
<Target Name="GenerateFSharpILLinkSubstitutions" AfterTargets="CreateManifestResourceNames"
Condition="'$(DisableILLinkSubstitutions)' != 'true' and '$(AssemblyName)' != 'FSharp.Core'
and '@(EmbeddedResource->WithMetadataValue('LogicalName', 'ILLink.Substitutions.xml'))' == ''
and '@(EmbeddedResource->WithMetadataValue('ManifestResourceName', 'ILLink.Substitutions.xml'))' == ''">
<GenerateILLinkSubstitutions
AssemblyName="$(AssemblyName)"
IntermediateOutputPath="$(IntermediateOutputPath)">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<linker>
<resource name="custom.marker" action="remove" />
</linker>
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

<ItemGroup>
<Compile Include="..\Program.fs" />
<EmbeddedResource Include="CustomSubstitutions.xml" LogicalName="ILLink.Substitutions.xml" />
<EmbeddedResource Include="..\Program.fs" LogicalName="custom.marker" />
</ItemGroup>

<Import Project="$(MSBuildThisFileDirectory)../../../../eng/Versions.props" />
Expand Down
11 changes: 10 additions & 1 deletion tests/AheadOfTime/Trimming/check.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ function CheckTrim($root, $tfm, $outputfile, $expected_len, $callerLineNumber) {

# Checking that the trimmed outputfile binary is of expected size (needs adjustments if test is updated).
$file = Get-Item (Join-Path $PSScriptRoot "${root}\bin\release\${tfm}\win-x64\publish\${outputfile}")
$metadata = [System.Reflection.Assembly]::LoadFile($file.FullName).GetManifestResourceNames() |
Where-Object { $_ -match '^FSharp(Signature|Optimization)' }
if ($metadata) {
$errors += "Metadata remains in ${outputfile}: $($metadata -join ', ')"
}
$app = [System.Reflection.Assembly]::LoadFile((Join-Path $file.DirectoryName "${root}.dll"))
if ($app.GetManifestResourceNames() -contains "custom.marker") {
$errors += "Custom resource-removal rule was not applied in ${root}"
}
$file_len = $file.Length
if ($expected_len -eq -1)
{
Expand Down Expand Up @@ -71,7 +80,7 @@ $allErrors += CheckTrim -root "SelfContained_Trimming_Test" -tfm "net9.0" -outpu
$allErrors += CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net9.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 9174528 -callerLineNumber 71

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

# Report all errors and exit with failure if any occurred
if ($allErrors.Count -gt 0) {
Expand Down
Loading
Loading