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
39 changes: 39 additions & 0 deletions azure-pipelines-PR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ stages:
env:
DOTNET_ROLL_FORWARD_TO_PRERELEASE: 1
displayName: Validate test projects are registered in TestSplit.fsx
- script: ./eng/common/dotnet.sh fsi eng/tests/AuditFSharpCoreTfmGuards.fsx
env:
DOTNET_ROLL_FORWARD_TO_PRERELEASE: 1
displayName: Audit FSharp.Core TFM (#if) guards use the standardized idiom

# Check whether package with current version has been published to nuget.org
# We will try to restore both FSharp.Core and FCS and if restore is _successful_, package version needs to be bumped.
Expand Down Expand Up @@ -186,6 +190,29 @@ stages:
DOTNET_ROLL_FORWARD_TO_PRERELEASE: 1
displayName: Check published package version

# Assembly-level ApiCompat identity gate: the shipped net TFM FSharp.Core must have the
# exact same public surface as the netstandard2.1 FSharp.Core (both from this build). This
# is the gate that makes the "net10.0 == netstandard2.1" invariant enforceable. It is a
# required, blocking job (no continueOnError) using the SDK's ValidateAssembliesTask in
# strict/bidirectional mode with an EMPTY suppression set.
- job: FSharpCore_ApiCompat
pool:
name: $(DncEngPublicBuildPool)
demands: ImageOverride -equals $(LinuxMachineQueueName)
steps:
- checkout: self
clean: true
- script: ./eng/common/dotnet.sh
displayName: Install .NET SDK
- script: ./eng/common/dotnet.sh build src/FSharp.Core/FSharp.Core.fsproj -c Release
env:
DOTNET_ROLL_FORWARD_TO_PRERELEASE: 1
displayName: Build FSharp.Core (all shipped TFMs, Release)
- script: ./eng/common/dotnet.sh msbuild tests/FSharp.Core.ApiCompat/FSharp.Core.ApiCompat.proj -t:ValidateFSharpCoreApiIdentity -p:Configuration=Release
env:
DOTNET_ROLL_FORWARD_TO_PRERELEASE: 1
displayName: ApiCompat net-vs-netstandard2.1 identity gate (strict, empty suppressions)


#-------------------------------------------------------------------------------------------------------------------#
# PR builds #
Expand Down Expand Up @@ -564,6 +591,8 @@ stages:
NativeToolsOnMachine: true
- script: .\tests\EndToEndBuildTests\EndToEndBuildTests.cmd -c Release
displayName: End to end build tests
- script: .\eng\common\dotnet.cmd fsi .\tests\FSharp.Core.PackageVerification\VerifyFSharpCorePackage.fsx
displayName: 'e2e-1: verify shipped FSharp.Core nupkg (lib layout, satellites, nuspec group, AssemblyVersion parity)'

# Publish artifacts for regression testing
- task: PublishPipelineArtifact@1
Expand Down Expand Up @@ -707,6 +736,16 @@ stages:
- powershell: $(Build.SourcesDirectory)/tests/AheadOfTime/check.ps1
displayName: Build, trim, publish and check the state of the trimmed app.
workingDirectory: $(Build.SourcesDirectory)/tests/AheadOfTime
- script: .\eng\common\dotnet.cmd fsi .\tests\AheadOfTime\NetTfmResolution\VerifyNetResolution.fsx
displayName: 'e2e-2: net-TFM consumer asset-resolution witness (assets.json + runtime smoke)'
workingDirectory: $(Build.SourcesDirectory)
- task: PublishPipelineArtifact@1
displayName: Publish e2e-2 resolution evidence (project.assets.json)
inputs:
targetPath: '$(Build.SourcesDirectory)/tests/AheadOfTime/NetTfmResolution/obj'
artifactName: 'e2e2 NetTfmResolution obj Attempt $(System.JobAttempt) $(_kind)'
continueOnError: true
condition: always()
- task: PublishPipelineArtifact@1
displayName: Publish Trim Tests Logs
inputs:
Expand Down
4 changes: 4 additions & 0 deletions docs/release-notes/.FSharp.Core/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@
* Fix `Array.exists2` documentation examples to use equal-length arrays; the previous examples would throw `ArgumentException` at runtime instead of returning the documented `false`/`true` values. ([PR #19672](https://github.com/dotnet/fsharp/pull/19672))
* Move `Async.StartChild` to the "Starting Async Computations" docs category alongside `Async.StartChildAsTask`. ([Issue #19667](https://github.com/dotnet/fsharp/issues/19667))
* Add `InlineIfLambda` to `Array.init` ([PR #19869](https://github.com/dotnet/fsharp/pull/19869))

### Added

* Ship `FSharp.Core` with an additional `net10.0` target framework (next to `netstandard2.0` and `netstandard2.1`). The `net`-TFM assembly is public-surface-identical to the `netstandard2.1` one; the target version is a pinned, deliberately advanced knob. ([PR #20224](https://github.com/dotnet/fsharp/pull/20224))
16 changes: 16 additions & 0 deletions eng/TargetFrameworks.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@

<!-- Derive major version by stripping 'net' prefix and '.0' suffix (e.g., net10.0 -> 10) -->
<FSharpNetCoreProductMajorVersion>$([System.Text.RegularExpressions.Regex]::Replace('$(FSharpNetCoreProductTargetFramework)', '^net(\d+)\.0$', '$1'))</FSharpNetCoreProductMajorVersion>

<!-- Pinned *shipped* net TFM for the FSharp.Core NuGet package. This LAGS the in-dev product TFM above and
tracks the latest STABLE released .NET. It is bumped deliberately (see docs / RFC and the plan's
"how to bump" checklist), NOT automatically with the product. Overridable via env var / CLI. -->
<FSharpCoreShippedNetTargetFramework Condition="'$(FSharpCoreShippedNetTargetFramework)' == ''">net10.0</FSharpCoreShippedNetTargetFramework>
<FSharpCoreShippedNetMajorVersion>$([System.Text.RegularExpressions.Regex]::Replace('$(FSharpCoreShippedNetTargetFramework)', '^net(\d+)\.0$', '$1'))</FSharpCoreShippedNetMajorVersion>
</PropertyGroup>

<!-- Enforce the pinned-stable invariant: the shipped FSharp.Core net TFM must be STRICTLY LESS than the
in-dev product TFM (so producing F# for net(N+1) still ships FSharp.Core targeting the stable netN).
Scoped to the FSharp.Core build/pack, the only consumer of the pin. -->
<Target Name="_ValidateFSharpCoreShippedNetTfm"
Condition="'$(MSBuildProjectName)' == 'FSharp.Core'"
BeforeTargets="Build;Pack">
<Error Condition="!$([MSBuild]::VersionLessThan('$(FSharpCoreShippedNetMajorVersion).0', '$(FSharpNetCoreProductMajorVersion).0'))"
Text="FSharpCoreShippedNetTargetFramework ($(FSharpCoreShippedNetTargetFramework)) must be STRICTLY LESS than the in-dev product TFM ($(FSharpNetCoreProductTargetFramework)). Bump the pin only when the product advances." />
</Target>

</Project>
138 changes: 138 additions & 0 deletions eng/tests/AuditFSharpCoreTfmGuards.fsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Audit of the TFM (#if) guards in src/FSharp.Core.
//
// Why this exists (the user's explicit "#ifdef management" ask): shipping FSharp.Core as an extra
// net TFM next to netstandard2.0/netstandard2.1 only stays correct if the TFM #if guards keep using
// the ONE standardized idiom. The dangerous regressions are:
// * a bare #if NETSTANDARD2_1 -> excludes the net TFM, silently dropping a feature there
// * a bare #if !NET -> drops a BCL polyfill on EVERY net TFM (wrong floor)
// * any NEW TFM #if that doesn't follow the reviewed idiom.
//
// The standardized idioms are:
// * feature available on ns2.1 AND all net: #if NETSTANDARD2_1_OR_GREATER || NET (or NETSTANDARD2_1 || NET)
// * BCL polyfill present only below a floor: #if !NET5_0_OR_GREATER / #if !NET8_0_OR_GREATER
//
// Mechanism: this is an ALLOW-LIST audit. Every TFM-discriminating #if / #elif guard in
// src/FSharp.Core/**/*.{fs,fsi} must appear in the allow-list below, keyed by <file>:<guard-expr>
// (NOT by line number). Any guard that is not allow-listed fails the audit, which forces a human to
// review the new guard and, if it is a legitimate use of the idiom, add it here. This catches the
// bare-NETSTANDARD2_1 and bare-!NET anti-patterns automatically (they are simply not allow-listed).
//
// Run: dotnet fsi eng/tests/AuditFSharpCoreTfmGuards.fsx
// Self-test: dotnet fsi eng/tests/AuditFSharpCoreTfmGuards.fsx --self-test
// Portability: uses `git ls-files` + the .NET regex engine (NOT `git grep -P`, which is unavailable
// on git builds without PCRE, e.g. macOS).

open System
open System.Diagnostics
open System.Text.RegularExpressions

let scopeGlobs = [ "src/FSharp.Core/*.fs"; "src/FSharp.Core/*.fsi" ]

// The reviewed, allow-listed TFM guards after standardization. Key = (repo-relative path with '/',
// normalized guard expression). To add an entry: confirm the guard uses the idiom above, then list
// it here with a one-line justification in the PR.
let allowList : Set<string * string> =
set [
// Collection-expression support ([<CollectionBuilder>] + Create(ReadOnlySpan<_>)): present on
// netstandard2.1 and every net TFM.
"src/FSharp.Core/set.fs", "NETSTANDARD2_1_OR_GREATER || NET"
"src/FSharp.Core/set.fsi", "NETSTANDARD2_1_OR_GREATER || NET"
"src/FSharp.Core/prim-types.fs", "NETSTANDARD2_1_OR_GREATER || NET"
"src/FSharp.Core/prim-types.fsi", "NETSTANDARD2_1_OR_GREATER || NET"
// task { use! ... } over IAsyncDisposable (TryFinallyAsync / TaskBuilderBase.Using): ns2.1 + net.
"src/FSharp.Core/tasks.fs", "NETSTANDARD2_1 || NET"
"src/FSharp.Core/tasks.fsi", "NETSTANDARD2_1 || NET"
// BCL polyfills that must be dropped once the BCL provides the type:
// DynamicallyAccessedMembers -> floor at NET5
// CollectionBuilder/ScopedRef -> floor at NET8
"src/FSharp.Core/prim-types.fs", "!NET5_0_OR_GREATER"
"src/FSharp.Core/prim-types.fsi", "!NET5_0_OR_GREATER"
"src/FSharp.Core/prim-types.fs", "!NET8_0_OR_GREATER"
"src/FSharp.Core/prim-types.fsi", "!NET8_0_OR_GREATER"
]

// A guard expression is "TFM-discriminating" if it mentions any of these tokens.
let tfmToken =
Regex(@"NETSTANDARD|NETCOREAPP|NETFRAMEWORK|NET\d|(?:^|[^0-9A-Za-z_])!?NET(?:[^0-9A-Za-z_]|$)",
RegexOptions.Compiled)

let guardLine = Regex(@"^\s*#(?:if|elif)\s+(?<expr>.*\S)\s*$", RegexOptions.Compiled)

let normalize (s: string) = Regex.Replace(s, @"\s+", " ").Trim()

let runGit (args: string) =
let psi = ProcessStartInfo("git", args, RedirectStandardOutput = true, UseShellExecute = false)
use p = Process.Start psi
let out = p.StandardOutput.ReadToEnd()
p.WaitForExit()
if p.ExitCode <> 0 then failwithf "git %s failed (exit %d)" args p.ExitCode
out

let trackedFiles () =
runGit ("ls-files -- " + String.Join(" ", scopeGlobs))
|> fun s -> s.Split([| '\n'; '\r' |], StringSplitOptions.RemoveEmptyEntries)
|> Array.map (fun p -> p.Trim())
|> Array.filter (fun p -> p <> "")

// Returns the TFM guards found in the given lines as (normalizedExpr) values.
let tfmGuardsIn (lines: string[]) =
lines
|> Array.choose (fun ln ->
let m = guardLine.Match ln
if m.Success then
let expr = normalize m.Groups.["expr"].Value
if tfmToken.IsMatch expr then Some expr else None
else None)

let auditRepo () =
let violations =
[ for file in trackedFiles () do
let path = file.Replace('\\', '/')
for expr in tfmGuardsIn (IO.File.ReadAllLines file) do
if not (allowList.Contains(path, expr)) then
yield path, expr ]
if violations.IsEmpty then
printfn "FSharp.Core TFM #if guard audit: OK (%d allow-listed guards)." allowList.Count
0
else
eprintfn "FSharp.Core TFM #if guard audit: FAILED. Non-allow-listed TFM guard(s):"
for (path, expr) in violations do
eprintfn " %s: #if %s" path expr
eprintfn ""
eprintfn "Use the standardized idiom (see eng/tests/AuditFSharpCoreTfmGuards.fsx header):"
eprintfn " * feature on ns2.1 + all net: #if NETSTANDARD2_1_OR_GREATER || NET"
eprintfn " * polyfill below a BCL floor: #if !NET8_0_OR_GREATER (or !NET5_0_OR_GREATER)"
eprintfn "A bare '#if NETSTANDARD2_1' or '#if !NET' is almost always wrong for the shipped net TFM."
eprintfn "If the new guard is a legitimate use of the idiom, add its <file>:<expr> to the allow-list."
1

// Self-test: prove the detector actually bites on the known anti-patterns and passes the good idioms.
let selfTest () =
let mutable ok = true
let check desc (expr: string) shouldBeTfm =
let isTfm = tfmToken.IsMatch(normalize expr)
if isTfm <> shouldBeTfm then
ok <- false
eprintfn " self-test FAIL: %s -> tfmToken=%b, expected %b" desc isTfm shouldBeTfm
// These MUST be recognized as TFM guards (and, not being allow-listed for a fake file, would fail):
check "bare NETSTANDARD2_1" "NETSTANDARD2_1" true
check "bare !NET" "!NET" true
check "NET5_0_OR_GREATER" "NET5_0_OR_GREATER" true
check "NETCOREAPP" "NETCOREAPP" true
check "good idiom || NET" "NETSTANDARD2_1_OR_GREATER || NET" true
// These are NOT TFM guards and must be ignored by the audit:
check "DEBUG" "DEBUG" false
check "FX_NO_SOMETHING" "FX_NO_SOMETHING" false
// Allow-list containment behaves as expected:
if not (allowList.Contains("src/FSharp.Core/tasks.fs", "NETSTANDARD2_1 || NET")) then
ok <- false; eprintfn " self-test FAIL: known-good pair not in allow-list"
if allowList.Contains("src/FSharp.Core/tasks.fs", "NETSTANDARD2_1") then
ok <- false; eprintfn " self-test FAIL: bare NETSTANDARD2_1 unexpectedly allow-listed"
if ok then
printfn "FSharp.Core TFM #if guard audit self-test: OK."; 0
else
eprintfn "FSharp.Core TFM #if guard audit self-test: FAILED."; 1

let args = Environment.GetCommandLineArgs()
let exitCode = if Array.contains "--self-test" args then selfTest () else auditRepo ()
exit exitCode
8 changes: 7 additions & 1 deletion src/FSharp.Core/FSharp.Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworks Condition="'$(Configuration)' == 'Proto'">netstandard2.0</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' != 'Proto'">netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' != 'Proto'">netstandard2.0;netstandard2.1;$(FSharpCoreShippedNetTargetFramework)</TargetFrameworks>
<NoWarn>$(NoWarn);75</NoWarn> <!-- InternalCommandLineOption -->
<NoWarn>$(NoWarn);1204</NoWarn> <!-- This construct is for use in the FSharp.Core library and should not be used directly -->
<AllowCrossTargeting>true</AllowCrossTargeting>
Expand Down Expand Up @@ -36,6 +36,12 @@
<Configurations>Debug;Release;Proto</Configurations>
</PropertyGroup>

<!-- Pinned .NET TFM for the shipped FSharp.Core, surfaced to FSharp.Core.nuspec as a $token$
so the package lib/dependency group stay driven by the single knob in eng/TargetFrameworks.props. -->
<ItemGroup Condition="'$(Configuration)' != 'Proto'">
<NuspecProperty Include="FSharpCoreShippedNetTargetFramework=$(FSharpCoreShippedNetTargetFramework)" />
</ItemGroup>

<!-- .FSharp.Core always uses the old style initialization mechanism because of SQL CLR requirements -->
<PropertyGroup Condition="'$(BUILDING_USING_DOTNET)' != 'true' and '$(Configuration)' != 'Proto'">
<OtherFlags>$(OtherFlags) --realsig-</OtherFlags>
Expand Down
7 changes: 7 additions & 0 deletions src/FSharp.Core/FSharp.Core.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<dependencies>
<group targetFramework=".NETStandard2.0" />
<group targetFramework=".NETStandard2.1" />
<group targetFramework="$FSharpCoreShippedNetTargetFramework$" />
</dependencies>
</metadata>
<files>
Expand All @@ -22,5 +23,11 @@

<!-- resources -->
<file src="FSharp.Core\$Configuration$\netstandard2.1\**\FSharp.Core.resources.dll" target="lib\netstandard2.1" />

<file src="FSharp.Core\$Configuration$\$FSharpCoreShippedNetTargetFramework$\FSharp.Core.dll" target="lib\$FSharpCoreShippedNetTargetFramework$" />
<file src="FSharp.Core\$Configuration$\$FSharpCoreShippedNetTargetFramework$\FSharp.Core.xml" target="lib\$FSharpCoreShippedNetTargetFramework$" />

<!-- resources -->
<file src="FSharp.Core\$Configuration$\$FSharpCoreShippedNetTargetFramework$\**\FSharp.Core.resources.dll" target="lib\$FSharpCoreShippedNetTargetFramework$" />
</files>
</package>
12 changes: 6 additions & 6 deletions src/FSharp.Core/Query.fs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ type QueryBuilder() =
member _.Head (source: QuerySource<'T, 'Q>) =
Enumerable.First source.Source

member _.Nth (source: QuerySource<'T, 'Q>, index) =
member _.Nth (source: QuerySource<'T, 'Q>, index: int) =
Enumerable.ElementAt (source.Source, index)

member _.Skip (source: QuerySource<'T, 'Q>, count) : QuerySource<'T, 'Q> =
Expand All @@ -116,7 +116,7 @@ type QueryBuilder() =
member _.SkipWhile (source: QuerySource<'T, 'Q>, predicate) : QuerySource<'T, 'Q> =
QuerySource (Enumerable.SkipWhile (source.Source, Func<_, _>(predicate)))

member _.Take (source: QuerySource<'T, 'Q>, count) : QuerySource<'T, 'Q> =
member _.Take (source: QuerySource<'T, 'Q>, count: int) : QuerySource<'T, 'Q> =
QuerySource (Enumerable.Take (source.Source, count))

member _.TakeWhile (source: QuerySource<'T, 'Q>, predicate) : QuerySource<'T, 'Q> =
Expand Down Expand Up @@ -475,8 +475,8 @@ module Query =
MakeOrCallContainsOrElementAt FQ FE

let MakeElementAt, CallElementAt =
let FQ = methodhandleof (fun (x, y) -> Queryable.ElementAt(x, y))
let FE = methodhandleof (fun (x, y) -> Enumerable.ElementAt(x, y))
let FQ = methodhandleof (fun (x, y) -> Queryable.ElementAt(x, (y: int)))
let FE = methodhandleof (fun (x, y) -> Enumerable.ElementAt(x, (y: int)))
MakeOrCallContainsOrElementAt FQ FE

let MakeOrCallMinByOrMaxBy FQ FE =
Expand Down Expand Up @@ -886,8 +886,8 @@ module Query =

let MakeTake =
MakeSkipOrTake
(methodhandleof (fun (x, y) -> Queryable.Take (x, y)))
(methodhandleof (fun (x, y) -> Enumerable.Take (x, y)))
(methodhandleof (fun (x, y) -> Queryable.Take (x, (y: int))))
(methodhandleof (fun (x, y) -> Enumerable.Take (x, (y: int))))

let MakeSkipWhile =
GenMakeSkipWhileOrTakeWhile
Expand Down
6 changes: 3 additions & 3 deletions src/FSharp.Core/local.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ module internal DetailedExceptions =
open Microsoft.FSharp.Core

/// takes an argument, a formatting string, a param array to splice into the formatting string
let inline invalidArgFmt (arg:string) (format:string) paramArray =
let inline invalidArgFmt (arg:string) (format:string) (paramArray: obj[]) =
let msg = String.Format (format, paramArray)
raise (ArgumentException(msg, arg))

/// takes an argument, a formatting string, a param array to splice into the formatting string
let inline invalidArgOutOfRangeFmt (arg:string) (format:string) paramArray =
let inline invalidArgOutOfRangeFmt (arg:string) (format:string) (paramArray: obj[]) =
let msg = String.Format (format, paramArray)
raise (ArgumentOutOfRangeException(arg, msg))

/// takes a formatting string and a param array to splice into the formatting string
let inline invalidOpFmt (format:string) paramArray =
let inline invalidOpFmt (format:string) (paramArray: obj[]) =
let msg = String.Format (format, paramArray)
raise (InvalidOperationException(msg))

Expand Down
Loading
Loading