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
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Core/11.0.100.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
### Fixed

* Mirror compiler-semantic attributes (e.g. `[<NoDynamicInvocation>]`, `[<Experimental>]`) in `.fsi` signature files to match `.fs` implementations. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880))
* Fix `Array2D` AOT compatibility: `create`, `init` and `rebase` no longer emit `IL3050`; `map`, `mapi` and `copy` no longer emit it on `net10.0`; the `*Based` operations now warn at the call site before the runtime failure they already had. ([Language Suggestion #1454](https://github.com/fsharp/fslang-suggestions/issues/1454), [PR #20338](https://github.com/dotnet/fsharp/pull/20338))
* 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))
Expand Down
60 changes: 45 additions & 15 deletions src/FSharp.Core/array2.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ open System
open Microsoft.FSharp.Core
open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators
open Microsoft.FSharp.Core.Operators.Checked
open System.Diagnostics.CodeAnalysis

#nowarn "3218" // mismatch of parameter name where 'count1' --> 'length1' would shadow function in module of same name

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
[<RequireQualifiedAccess>]
module Array2D =

[<Literal>]
let private nonZeroBasedRequiresDynamicCode =
"Arrays with a non-zero lower bound cannot be created in an AOT-compiled application. Use Array2D.zeroCreate, Array2D.create or Array2D.init instead."

let inline checkNonNull argName arg =
if isNull arg then
nullArg argName
Expand Down Expand Up @@ -48,37 +53,62 @@ module Array2D =
if length2 < 0 then invalidArgInputMustBeNonNegative "length2" length2
(# "newarr.multi 2 !0" type ('T) length1 length2 : 'T[,] #)

[<CompiledName("ZeroCreateBased")>]
let zeroCreateBased (base1:int) (base2:int) (length1:int) (length2:int) =
// map, mapi and copy must stay on this unannotated path; the public based entry points
// carry RequiresDynamicCode instead.
let private zeroCreateBasedAux (base1:int) (base2:int) (length1:int) (length2:int) : 'T[,] =
if base1 = 0 && base2 = 0 then
zeroCreate length1 length2
else
#if NET9_0_OR_GREATER
// Unlike Array.CreateInstance, this overload carries no RequiresDynamicCode.
(Array.CreateInstanceFromArrayType(typeof<'T[,]>, [|length1;length2|], [|base1;base2|]) :?> 'T[,])
#else
(Array.CreateInstance(typeof<'T>, [|length1;length2|], [|base1;base2|]) :?> 'T[,])
#endif

let private initBasedAux base1 base2 length1 length2 initializer : 'T[,] =
let array = zeroCreateBasedAux base1 base2 length1 length2
let f = OptimizedClosures.FSharpFunc<_,_,_>.Adapt(initializer)
for i = base1 to base1 + length1 - 1 do
for j = base2 to base2 + length2 - 1 do
array.[i, j] <- f.Invoke(i, j)
array

[<RequiresDynamicCode(nonZeroBasedRequiresDynamicCode)>]
[<CompiledName("ZeroCreateBased")>]
let zeroCreateBased (base1:int) (base2:int) (length1:int) (length2:int) : 'T[,] =
zeroCreateBasedAux base1 base2 length1 length2

[<RequiresDynamicCode(nonZeroBasedRequiresDynamicCode)>]
[<CompiledName("CreateBased")>]
let createBased base1 base2 length1 length2 (initial:'T) =
let array = (zeroCreateBased base1 base2 length1 length2 : 'T[,])
let array = (zeroCreateBasedAux base1 base2 length1 length2 : 'T[,])
for i = base1 to base1 + length1 - 1 do
for j = base2 to base2 + length2 - 1 do
array.[i, j] <- initial
array

[<RequiresDynamicCode(nonZeroBasedRequiresDynamicCode)>]
[<CompiledName("InitializeBased")>]
let initBased base1 base2 length1 length2 initializer =
let array = (zeroCreateBased base1 base2 length1 length2 : 'T[,])
let f = OptimizedClosures.FSharpFunc<_,_,_>.Adapt(initializer)
for i = base1 to base1 + length1 - 1 do
for j = base2 to base2 + length2 - 1 do
array.[i, j] <- f.Invoke(i, j)
array
let initBased base1 base2 length1 length2 initializer : 'T[,] =
initBasedAux base1 base2 length1 length2 initializer

[<CompiledName("Create")>]
let create length1 length2 (value:'T) =
createBased 0 0 length1 length2 value
let array = zeroCreate length1 length2
for i = 0 to length1 - 1 do
for j = 0 to length2 - 1 do
array.[i, j] <- value
array

[<CompiledName("Initialize")>]
let init length1 length2 initializer =
initBased 0 0 length1 length2 initializer
let array = zeroCreate length1 length2
let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt(initializer)
for i = 0 to length1 - 1 do
for j = 0 to length2 - 1 do
array.[i, j] <- f.Invoke(i, j)
array

[<CompiledName("Iterate")>]
let iter action array =
Expand Down Expand Up @@ -106,18 +136,18 @@ module Array2D =
[<CompiledName("Map")>]
let map mapping array =
checkNonNull "array" array
initBased (base1 array) (base2 array) (length1 array) (length2 array) (fun i j -> mapping array.[i,j])
initBasedAux (base1 array) (base2 array) (length1 array) (length2 array) (fun i j -> mapping array.[i,j])

[<CompiledName("MapIndexed")>]
let mapi mapping array =
checkNonNull "array" array
let f = OptimizedClosures.FSharpFunc<_, _, _, _>.Adapt(mapping)
initBased (base1 array) (base2 array) (length1 array) (length2 array) (fun i j -> f.Invoke(i, j, array.[i,j]))
initBasedAux (base1 array) (base2 array) (length1 array) (length2 array) (fun i j -> f.Invoke(i, j, array.[i,j]))

[<CompiledName("Copy")>]
let copy array =
checkNonNull "array" array
initBased (base1 array) (base2 array) (length1 array) (length2 array) (fun i j -> array.[i,j])
initBasedAux (base1 array) (base2 array) (length1 array) (length2 array) (fun i j -> array.[i,j])

[<CompiledName("Rebase")>]
let rebase array =
Expand Down
18 changes: 18 additions & 0 deletions src/FSharp.Core/prim-types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,24 @@ namespace System.Diagnostics.CodeAnalysis

#endif

#if !NET7_0_OR_GREATER
namespace System.Diagnostics.CodeAnalysis

open System
open Microsoft.FSharp.Core

/// <summary>
/// Indicates that the specified member requires the ability to generate new code at runtime.
/// </summary>
[<AttributeUsage(AttributeTargets.Class ||| AttributeTargets.Constructor ||| AttributeTargets.Method,
Inherited = false)>]
type internal RequiresDynamicCodeAttribute (message: string) =
inherit Attribute ()

member _.Message = message

#endif

namespace Microsoft.FSharp.Core
open System
open System.Collections
Expand Down
18 changes: 18 additions & 0 deletions src/FSharp.Core/prim-types.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,24 @@ namespace System.Diagnostics.CodeAnalysis

#endif

#if !NET7_0_OR_GREATER
namespace System.Diagnostics.CodeAnalysis

open System
open Microsoft.FSharp.Core

/// <summary>
/// Indicates that the specified member requires the ability to generate new code at runtime.
/// </summary>
[<AttributeUsage(AttributeTargets.Class ||| AttributeTargets.Constructor ||| AttributeTargets.Method,
Inherited = false)>]
type internal RequiresDynamicCodeAttribute =
inherit Attribute
new: string -> RequiresDynamicCodeAttribute
member Message: string

#endif

namespace Microsoft.FSharp.Core

open System
Expand Down
56 changes: 56 additions & 0 deletions tests/AheadOfTime/NativeAOT/Array2DProgram.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module Array2DProgram

open System

let private check name (actual: int) (expected: int) =
if actual <> expected then
Console.WriteLine $"FAILED {name}: expected {expected} but got {actual}"

// Consume every grid, so ILC cannot drop the allocations this test exists to exercise.
let private sum (grid: int[,]) =
let total = ref 0
grid |> Array2D.iteri (fun _ _ v -> total.Value <- total.Value + v)
total.Value

[<System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("AOT", "IL3050",
Justification = "Deliberately calls the unsupported API to assert it throws PlatformNotSupportedException under AOT.")>]
let private createBasedThrows rows cols =
try
Array2D.createBased 1 2 rows cols 7 |> ignore
0
with :? PlatformNotSupportedException ->
1

let run (argv: string[]) =
// Derive the dimensions from argv, so ILC cannot constant-fold the allocations away.
// The expected values below assume check.ps1 runs the app with no arguments.
let rows = 3 + argv.Length
let cols = 4 + argv.Length

let table = Array2D.init rows cols (fun i j -> (i + 1) * (j + 1))
check "init.rows" (Array2D.length1 table) 3
check "init.cols" (Array2D.length2 table) 4
check "init" (sum table) 60

let board = Array2D.create rows cols 2
check "create" (sum board) 24

Array2D.blit table 0 0 board 1 1 2 2
check "blit" (sum board) 25
check "blit.cell" board.[2, 2] 4

let rebased = Array2D.rebase table
check "rebase" (sum rebased) 60
check "rebase.base" (Array2D.base1 rebased + Array2D.base2 rebased) 0

let doubled = Array2D.map (fun v -> v * 2) table
check "map" (sum doubled) 120

let skewed = Array2D.mapi (fun i j v -> v + i - j) table
check "mapi" (sum skewed) 54

let copied = Array2D.copy table
check "copy" (sum copied) 60
check "copy.distinct" (if Object.ReferenceEquals(copied, table) then 1 else 0) 0

check "createBased.throws" (createBasedThrows rows cols) 1
3 changes: 2 additions & 1 deletion tests/AheadOfTime/NativeAOT/NativeAOT_Test.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<TargetFramework>$(FSharpCoreShippedNetTargetFramework)</TargetFramework>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
Expand All @@ -24,6 +24,7 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="Array2DProgram.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

Expand Down
3 changes: 2 additions & 1 deletion tests/AheadOfTime/NativeAOT/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ let runChecks () =
// check ($"value = %A{x}", "value = 42")

[<EntryPoint>]
let main _ =
let main argv =
runChecks ()
Array2DProgram.run argv
// Success sentinel; a failed check above printed a "FAILED" line first, so the output won't be just this.
Console.WriteLine "Finished"
0
33 changes: 16 additions & 17 deletions tests/AheadOfTime/NativeAOT/check.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,41 @@
$ErrorActionPreference = "Stop"

$root = "NativeAOT_Test"
$tfm = "net10.0"

$cwd = Get-Location
Set-Location $PSScriptRoot

dotnet publish -restore -c release -f:$tfm "$root.fsproj" -bl:"$PSScriptRoot/../../../artifacts/log/Release/AheadOfTime/NativeAOT/$root.binlog"
dotnet publish -restore -c release "$root.fsproj" -bl:"$PSScriptRoot/../../../artifacts/log/Release/AheadOfTime/NativeAOT/$root.binlog"
if (-not ($LASTEXITCODE -eq 0)) {
Set-Location $cwd
Write-Error "NativeAOT publish failed with exit code $LASTEXITCODE" -ErrorAction Stop
}

# Prove this is a genuine net10 dogfood: a net10.0 consumer must resolve the locally-packed
# FSharp.Core's lib/net10.0 asset, not the netstandard2.1 fallback. Assert on the *selected*
# compile/runtime asset under "targets" (the "libraries" manifest lists every lib folder, so a
# plain substring search would false-pass even when ns2.1 was chosen). Without this the test would
# still succeed on the ns2.1 asset and the net10 target framework would go unexercised.
# Read the frameworks from the project; passing -f above would override TargetFramework globally.
$props = dotnet msbuild "$root.fsproj" -getProperty:TargetFramework -getProperty:FSharpCoreShippedNetTargetFramework -nologo | ConvertFrom-Json
$tfm = $props.Properties.TargetFramework
$coreAsset = "lib/$($props.Properties.FSharpCoreShippedNetTargetFramework)/FSharp.Core.dll"

# Assert on the selected compile/runtime asset, not the "libraries" manifest, which lists every
# lib folder and would false-pass on the netstandard2.1 fallback.
$assets = Join-Path $PSScriptRoot "obj/project.assets.json"
$assetsJson = Get-Content $assets -Raw | ConvertFrom-Json
$net10Selected = $false
$netAssetSelected = $false
foreach ($target in $assetsJson.targets.PSObject.Properties) {
foreach ($package in $target.Value.PSObject.Properties) {
if ($package.Name -like "FSharp.Core/*") {
$compileKeys = if ($package.Value.compile) { @($package.Value.compile.PSObject.Properties.Name) } else { @() }
$runtimeKeys = if ($package.Value.runtime) { @($package.Value.runtime.PSObject.Properties.Name) } else { @() }
# FSharp.Core packs only lib/{tfm} (no ref/, no runtimes/), so compile and runtime resolve
# from the same folder; require both to be net10 - anything else means a fallback was chosen.
if (($compileKeys -contains "lib/net10.0/FSharp.Core.dll") -and
($runtimeKeys -contains "lib/net10.0/FSharp.Core.dll")) {
$net10Selected = $true
# FSharp.Core packs only lib/{tfm}, so both must resolve there; anything else is a fallback.
if (($compileKeys -contains $coreAsset) -and ($runtimeKeys -contains $coreAsset)) {
$netAssetSelected = $true
}
}
}
}
if (-not $net10Selected) {
if (-not $netAssetSelected) {
Set-Location $cwd
Write-Error "The net10.0 consumer did not resolve the lib/net10.0 FSharp.Core asset (it fell back to a lower target framework). The net10 FSharp.Core target framework is not being dogfooded. See $assets." -ErrorAction Stop
Write-Error "The $tfm consumer did not resolve the $coreAsset FSharp.Core asset (it fell back to a lower target framework). The shipped net FSharp.Core target framework is not being dogfooded. See $assets." -ErrorAction Stop
}

$exe = Join-Path $PSScriptRoot "bin/release/$tfm/win-x64/publish/$root.exe"
Expand All @@ -58,7 +57,7 @@ if (-not ($exitCode -eq 0)) {
}

if ($output.Trim() -ne "Finished") {
Write-Error "NativeAOT interpolation checks failed.`nOutput:`n$output" -ErrorAction Stop
Write-Error "NativeAOT behavior checks failed.`nOutput:`n$output" -ErrorAction Stop
}

Write-Host "NativeAOT interpolated-string test passed."
Write-Host "NativeAOT test passed."
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ Array3D.get array3D 0 0 0 |> Array3D.set array3D 0 0 0
// Array4D Renamings
(Array4D.length1 array4D, Array4D.length2 array4D, Array4D.length3 array4D, Array4D.length4 array4D) |> ignore
Array4D.get array4D 0 0 0 0 |> Array4D.set array4D 0 0 0 0

// Array2D.init renaming, appended here rather than above to limit IL baseline churn.
Array2D.init 2 2 (fun i j -> i + j) |> ignore
Loading
Loading