diff --git a/azure-pipelines-PR.yml b/azure-pipelines-PR.yml index 830df465998..507f6f79ffe 100644 --- a/azure-pipelines-PR.yml +++ b/azure-pipelines-PR.yml @@ -919,3 +919,23 @@ stages: buildScript: build.sh displayName: FsharpPlus_Net10_Linux useVmImage: $(UbuntuMachineQueueName) + - repo: TheAngryByrd/IcedTasks + commit: 863bf91cdee93d8c4c875bb5d321dd92eb20d5a9 + buildScript: dotnet build IcedTasks.sln + displayName: IcedTasks_Build + - repo: TheAngryByrd/IcedTasks + commit: 863bf91cdee93d8c4c875bb5d321dd92eb20d5a9 + buildScript: dotnet test IcedTasks.sln + displayName: IcedTasks_Test + - repo: demystifyfp/FsToolkit.ErrorHandling + commit: 9cd957e335767df03e2fb0aa2f7b0fed782c5091 + buildScript: dotnet build FsToolkit.ErrorHandling.sln + displayName: FsToolkit_ErrorHandling_Build + - repo: demystifyfp/FsToolkit.ErrorHandling + commit: 9cd957e335767df03e2fb0aa2f7b0fed782c5091 + buildScript: dotnet test FsToolkit.ErrorHandling.sln + displayName: FsToolkit_ErrorHandling_Test + - repo: opentk/opentk + commit: 60c20cca65a7df6e8335e8d6060d91b30909fbea + buildScript: dotnet build tests/OpenTK.Tests/OpenTK.Tests.fsproj -c Release ;; dotnet build tests/OpenTK.Tests.Integration/OpenTK.Tests.Integration.fsproj -c Release + displayName: OpenTK_FSharp_Build diff --git a/docs/regression-fs0229-bstream-misalignment.md b/docs/regression-fs0229-bstream-misalignment.md new file mode 100644 index 00000000000..0711b6feb0a --- /dev/null +++ b/docs/regression-fs0229-bstream-misalignment.md @@ -0,0 +1,140 @@ +# Regression: FS0229 B-Stream Misalignment in TypedTreePickle + +## Summary + +A metadata unpickling regression causes `FS0229` errors when the F# compiler (post-nullness-checking merge) reads metadata from assemblies compiled with `LangVersion < 9.0`. The root cause is a stream alignment bug in `TypedTreePickle.fs` where the secondary metadata stream ("B-stream") gets out of sync between writer and reader. + +## Error Manifestation + +``` +error FS0229: Error reading/writing metadata for assembly '': + The data read from the stream is inconsistent, reading past end of resource, + u_ty - 4/B OR u_ty - 1/B, byte = +``` + +This error occurs when consuming metadata from an assembly where: +1. The assembly was compiled by the current compiler (which writes B-stream data) +2. The compilation used `LangVersion 8.0` or earlier (which disables `langFeatureNullness`) +3. The assembly references BCL types whose type parameters carry `NotSupportsNull` or `AllowsRefStruct` constraints + +**Affected real-world library**: [FsToolkit.ErrorHandling](https://github.com/demystifyfp/FsToolkit.ErrorHandling), which uses `8.0` for `netstandard2.0`/`netstandard2.1` TFMs. + +## Root Cause + +### Dual-Stream Metadata Format + +F# compiler metadata uses two serialization streams: +- **Stream A** (main): Type tags, type constructor references, type parameter references, etc. +- **Stream B** (secondary): Nullness information per type + newer constraint data (e.g., `NotSupportsNull`, `AllowsRefStruct`) + +These streams are written in parallel during pickling and read in parallel during unpickling. The invariant is: **every byte written to stream B by the writer must have a corresponding read in the reader**. + +### The Bug + +In `p_ty2` (the type pickle function), nullness information is written to stream B **conditionally**: + +```fsharp +// BEFORE FIX (buggy) +| TType_app(tc, tinst, nullness) -> + if st.oglobals.langFeatureNullness then + match nullness.Evaluate() with + | NullnessInfo.WithNull -> p_byteB 12 st + | NullnessInfo.WithoutNull -> p_byteB 13 st + | NullnessInfo.AmbivalentToNull -> p_byteB 14 st + // No else branch - B-stream byte skipped when langFeatureNullness = false! + p_byte 2 st + p_tcref "typ" tc st + p_tys tinst st +``` + +But in `u_ty` (the type unpickle function), the B-stream byte is read **unconditionally**: + +```fsharp +| 2 -> + let tagB = u_byteB st // Always reads, regardless of langFeatureNullness at compile time + let tcref = u_tcref st + let tinst = u_tys st + match tagB with + | 0 -> TType_app(tcref, tinst, KnownAmbivalentToNull) + | 12 -> TType_app(tcref, tinst, KnownWithNull) + ... +``` + +This affects type tags 1 (TType_app no args), 2 (TType_app), 3 (TType_fun), and 4 (TType_var). + +Meanwhile, `p_tyar_constraints` **unconditionally** writes constraint data to B-stream: + +```fsharp +let p_tyar_constraints cxs st = + let cxs1, cxs2 = cxs |> List.partition (function + | TyparConstraint.NotSupportsNull _ | TyparConstraint.AllowsRefStruct _ -> false + | _ -> true) + p_list p_tyar_constraint cxs1 st + p_listB p_tyar_constraintB cxs2 st // Always writes to B, regardless of langFeatureNullness +``` + +### Misalignment Cascade + +When `langFeatureNullness = false`: + +1. Writer processes types → skips B-bytes for each type tag 1-4 +2. Writer processes type parameter constraints → writes `NotSupportsNull` data to B-stream (value `0x01`) +3. Reader processes types → reads B-stream expecting nullness tags → gets constraint data instead +4. Constraint byte `0x01` is not a valid nullness tag (valid values: 0, 9-20) → `ufailwith "u_ty - 4/B"` or similar + +The misalignment cascades: once one byte is read from the wrong position, all subsequent B-stream reads are shifted. + +## Fix + +Added `else p_byteB 0 st` to all four type cases in `p_ty2`, ensuring a B-byte is always written regardless of `langFeatureNullness`: + +```fsharp +// AFTER FIX +| TType_app(tc, tinst, nullness) -> + if st.oglobals.langFeatureNullness then + match nullness.Evaluate() with + | NullnessInfo.WithNull -> p_byteB 12 st + | NullnessInfo.WithoutNull -> p_byteB 13 st + | NullnessInfo.AmbivalentToNull -> p_byteB 14 st + else + p_byteB 0 st // Keep B-stream aligned + p_byte 2 st + p_tcref "typ" tc st + p_tys tinst st +``` + +Value `0` means "no nullness info / AmbivalentToNull" and is already handled by all reader match cases. + +## Timeline + +| Date | PR | Change | +|------|-----|--------| +| Jul 2024 | [#15181](https://github.com/dotnet/fsharp/pull/15181) | Nullness checking: introduced B-stream for nullness bytes, conditional write in `p_ty2` | +| Aug 2024 | [#15310](https://github.com/dotnet/fsharp/pull/15310) | Nullness checking applied to codebase | +| Sep 2024 | [#17706](https://github.com/dotnet/fsharp/pull/17706) | `AllowsRefStruct`: added constraint data to B-stream unconditionally via `p_listB` | + +The bug was latent from #15181 but only manifested when #17706 added unconditional B-stream writes for constraints. Before #17706, the B-stream was empty when `langFeatureNullness = false`, so the reader's unconditional reads would hit the end-of-stream sentinel (returning 0) harmlessly. After #17706, constraint data appeared in the B-stream even without nullness, causing the misalignment. + +## Regression Tests + +Two tests added in `tests/FSharp.Compiler.ComponentTests/Import/ImportTests.fs`: + +1. **Basic test**: Compiles a library with `LangVersion=8.0` containing generic types with BCL constraints (e.g., `IEquatable<'T>`), then references it from another compilation and verifies no FS0229 error. + +2. **Stress test**: Multiple type parameters with various constraint patterns, function types, and nested generics — all compiled at `LangVersion=8.0` and successfully consumed. + +## Reproduction + +To reproduce the original bug (before fix): + +1. Clone [FsToolkit.ErrorHandling](https://github.com/demystifyfp/FsToolkit.ErrorHandling) +2. Inject the pre-fix compiler via `UseLocalCompiler.Directory.Build.props` +3. Build `netstandard2.0` TFM (uses `LangVersion=8.0`) +4. Build `net9.0` TFM that references the `netstandard2.0` output +5. The `net9.0` build fails with `FS0229: u_ty - 4/B` + +## Files Changed + +- `src/Compiler/TypedTree/TypedTreePickle.fs` — Added `else p_byteB 0 st` to four locations in `p_ty2` +- `tests/FSharp.Compiler.ComponentTests/Import/ImportTests.fs` — Two regression tests +- `tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj` — Added `ImportTests.fs` include (was missing since migration) diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md index c247da5870b..cc417b8fd81 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md @@ -1,5 +1,8 @@ ### Fixed +* Fix FS0229 B-stream misalignment when reading metadata from assemblies compiled with LangVersion < 9.0, introduced by [#17706](https://github.com/dotnet/fsharp/pull/17706). ([PR #19260](https://github.com/dotnet/fsharp/pull/19260)) +* Fix FS3356 false positive for instance extension members with same name on different types, introduced by [#18821](https://github.com/dotnet/fsharp/pull/18821). ([PR #19260](https://github.com/dotnet/fsharp/pull/19260)) + ### Added ### Changed diff --git a/eng/templates/regression-test-jobs.yml b/eng/templates/regression-test-jobs.yml index 66c184138aa..2032c351731 100644 --- a/eng/templates/regression-test-jobs.yml +++ b/eng/templates/regression-test-jobs.yml @@ -1,5 +1,10 @@ # Template for F# Compiler Regression Tests # Tests third-party F# projects with the freshly built compiler +# +# buildScript notation: +# - Single command: buildScript: dotnet build MySolution.sln +# - Multiple commands: buildScript: dotnet build A.fsproj ;; dotnet build B.fsproj +# Commands separated by ';;' run sequentially; the job fails on the first non-zero exit code. parameters: - name: testMatrix @@ -10,7 +15,7 @@ parameters: jobs: - ${{ each item in parameters.testMatrix }}: - - job: RegressionTest_${{ replace(item.displayName, '-', '_') }}_${{ replace(replace(item.repo, '/', '_'), '-', '_') }} + - job: RegressionTest_${{ replace(replace(item.displayName, '-', '_'), '.', '_') }}_${{ replace(replace(replace(item.repo, '/', '_'), '-', '_'), '.', '_') }} displayName: '${{ item.displayName }} Regression Test' dependsOn: ${{ parameters.dependsOn }} ${{ if item.useVmImage }}: @@ -61,18 +66,22 @@ jobs: Get-ChildItem -Name $buildScript = '${{ item.buildScript }}' - if ($buildScript -like "dotnet*") { - Write-Host "Build command is a built-in dotnet command: $buildScript" - Write-Host "Skipping file existence check for built-in command" - } else { - Write-Host "Verifying build script exists: $buildScript" - if (Test-Path $buildScript) { - Write-Host "Build script found: $buildScript" + # Support ';;' separator for multiple commands — validate each command's script file + $commands = $buildScript -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + foreach ($cmd in $commands) { + if ($cmd -like "dotnet*") { + Write-Host "Built-in dotnet command, skipping file check: $cmd" } else { - Write-Host "Build script not found: $buildScript" - Write-Host "Available files in root:" - Get-ChildItem - exit 1 + $scriptFile = ($cmd -split ' ', 2)[0] + Write-Host "Verifying build script exists: $scriptFile" + if (Test-Path $scriptFile) { + Write-Host "Build script found: $scriptFile" + } else { + Write-Host "Build script not found: $scriptFile" + Write-Host "Available files in root:" + Get-ChildItem + exit 1 + } } } displayName: Checkout ${{ item.displayName }} at specific commit @@ -95,6 +104,13 @@ jobs: version: '8.0.x' installationPath: $(Pipeline.Workspace)/TestRepo/.dotnet + - task: UseDotNet@2 + displayName: Install .NET SDK 9.0.x for ${{ item.displayName }} + inputs: + packageType: sdk + version: '9.0.x' + installationPath: $(Pipeline.Workspace)/TestRepo/.dotnet + - task: UseDotNet@2 displayName: Install .NET SDK 10.0.100 for ${{ item.displayName }} inputs: @@ -140,6 +156,18 @@ jobs: Write-Host "===========================================" displayName: Report build environment for ${{ item.displayName }} + - pwsh: | + Set-Location $(Pipeline.Workspace)/TestRepo + if (Test-Path ".config/dotnet-tools.json") { + Write-Host "Restoring dotnet tools..." + dotnet tool restore + Write-Host "Tool restore exit code: $LASTEXITCODE" + } else { + Write-Host "No dotnet-tools.json found, skipping tool restore" + } + displayName: Restore dotnet tools for ${{ item.displayName }} + continueOnError: true + - pwsh: | Set-Location $(Pipeline.Workspace)/TestRepo Write-Host "============================================" @@ -150,7 +178,6 @@ jobs: Write-Host "============================================" Write-Host "" - $buildScript = '${{ item.buildScript }}' $errorLogPath = "$(Pipeline.Workspace)/build-errors.log" $fullLogPath = "$(Pipeline.Workspace)/build-full.log" @@ -168,41 +195,56 @@ jobs: Add-Content -Path $errorLogPath -Value $line } } - - if ($buildScript -like "dotnet*") { - Write-Host "Executing built-in command: $buildScript" - if ($IsWindows) { - cmd /c $buildScript 2>&1 | Tee-Object -FilePath $fullLogPath | ForEach-Object { + + function Run-Command { + param([string]$cmd) + if ($cmd -like "dotnet*") { + Write-Host "Executing built-in command: $cmd" + if ($IsWindows) { + cmd /c $cmd 2>&1 | Tee-Object -FilePath $fullLogPath -Append | ForEach-Object { + Process-BuildOutput $_ + } + } else { + bash -c "$cmd" 2>&1 | Tee-Object -FilePath $fullLogPath -Append | ForEach-Object { + Process-BuildOutput $_ + } + } + } elseif ($IsWindows) { + Write-Host "Executing file-based script: $cmd" + cmd /c ".\$cmd" 2>&1 | Tee-Object -FilePath $fullLogPath -Append | ForEach-Object { Process-BuildOutput $_ } } else { - bash -c "$buildScript" 2>&1 | Tee-Object -FilePath $fullLogPath | ForEach-Object { + Write-Host "Executing file-based script: $cmd" + $scriptFile = ($cmd -split ' ', 2)[0] + chmod +x "$scriptFile" + bash -c "./$cmd" 2>&1 | Tee-Object -FilePath $fullLogPath -Append | ForEach-Object { Process-BuildOutput $_ } } - } elseif ($IsWindows) { - Write-Host "Executing file-based script: $buildScript" - & ".\$buildScript" 2>&1 | Tee-Object -FilePath $fullLogPath | ForEach-Object { - Process-BuildOutput $_ - } - } else { - Write-Host "Executing file-based script: $buildScript" - chmod +x "$buildScript" - bash -c "./$buildScript" 2>&1 | Tee-Object -FilePath $fullLogPath | ForEach-Object { - Process-BuildOutput $_ + return $LASTEXITCODE + } + + # Support ';;' separator for multiple commands + $commands = ('${{ item.buildScript }}' -split ';;') | ForEach-Object { $_.Trim() } | Where-Object { $_ } + + foreach ($cmd in $commands) { + $exitCode = Run-Command $cmd + if ($exitCode -ne 0) { + Write-Host "" + Write-Host "============================================" + Write-Host "Command failed: $cmd" + Write-Host "Exit code: $exitCode" + Write-Host "============================================" + exit $exitCode } } - $exitCode = $LASTEXITCODE Write-Host "" Write-Host "============================================" Write-Host "Build completed for ${{ item.displayName }}" - Write-Host "Exit code: $exitCode" + Write-Host "Exit code: 0" Write-Host "============================================" - - if ($exitCode -ne 0) { - exit $exitCode - } displayName: Build ${{ item.displayName }} with local F# compiler env: LocalFSharpCompilerPath: $(Pipeline.Workspace)/FSharpCompiler diff --git a/src/Compiler/Checking/PostInferenceChecks.fs b/src/Compiler/Checking/PostInferenceChecks.fs index d3854d1ce27..e59a80b5cdf 100644 --- a/src/Compiler/Checking/PostInferenceChecks.fs +++ b/src/Compiler/Checking/PostInferenceChecks.fs @@ -2680,21 +2680,27 @@ let CheckEntityDefns cenv env tycons = // check modules //-------------------------------------------------------------------------- -/// Check for duplicate extension member names that would cause IL conflicts. -/// Extension members for types with the same simple name but different fully qualified names -/// will be emitted into the same IL container type, causing a duplicate member error. +/// Check for duplicate static extension member names that would cause IL conflicts. +/// Static extension members for types with the same simple name but different fully qualified names +/// compile to static methods in the same module IL type without a distinguishing first parameter, +/// so they can produce duplicate IL method signatures. +/// Instance extension members are safe because they compile with the extended type as the first +/// parameter, which differentiates the IL signatures. let CheckForDuplicateExtensionMemberNames (cenv: cenv) (vals: Val seq) = if cenv.reportErrors then - let extensionMembers = + let staticExtensionMembers = vals - |> Seq.filter (fun v -> v.IsExtensionMember && v.IsMember) + |> Seq.filter (fun v -> + v.IsExtensionMember + && v.IsMember + && not (v.IsInstanceMember)) |> Seq.toList - if not extensionMembers.IsEmpty then + if not staticExtensionMembers.IsEmpty then // Group by LogicalName which includes generic arity suffix (e.g., Expr`1 for Expr<'T>) // This matches how types are compiled to IL, so Expr and Expr<'T> are separate groups let groupedByLogicalName = - extensionMembers + staticExtensionMembers |> List.groupBy (fun v -> v.MemberApparentEntity.LogicalName) for (logicalName, members) in groupedByLogicalName do diff --git a/src/Compiler/TypedTree/TypedTreePickle.fs b/src/Compiler/TypedTree/TypedTreePickle.fs index 8a61809ab06..b5bd769de7f 100644 --- a/src/Compiler/TypedTree/TypedTreePickle.fs +++ b/src/Compiler/TypedTree/TypedTreePickle.fs @@ -2415,6 +2415,19 @@ let u_tyar_spec st = let u_tyar_specs = (u_list u_tyar_spec) +/// Write nullness information to stream B for a type. +/// Always writes exactly one byte to keep stream B aligned with unconditional reads in u_ty. +/// Other data (e.g. typar constraints) is also written to stream B unconditionally, +/// so skipping nullness bytes would cause stream B misalignment when langFeatureNullness = false. +let p_nullnessB baseTag (nullness: Nullness) st = + if st.oglobals.langFeatureNullness then + match nullness.Evaluate() with + | NullnessInfo.WithNull -> p_byteB baseTag st + | NullnessInfo.WithoutNull -> p_byteB (baseTag + 1) st + | NullnessInfo.AmbivalentToNull -> p_byteB (baseTag + 2) st + else + p_byteB 0 st + let _ = fill_p_ty2 (fun isStructThisArgPos ty st -> let ty = stripTyparEqns ty @@ -2437,45 +2450,25 @@ let _ = p_tys l st | TType_app(ERefNonLocal nleref, [], nullness) -> - if st.oglobals.langFeatureNullness then - match nullness.Evaluate() with - | NullnessInfo.WithNull -> p_byteB 9 st - | NullnessInfo.WithoutNull -> p_byteB 10 st - | NullnessInfo.AmbivalentToNull -> p_byteB 11 st - + p_nullnessB 9 nullness st // B tags: 9=WithNull, 10=WithoutNull, 11=Ambivalent p_byte 1 st p_simpletyp nleref st | TType_app(tc, tinst, nullness) -> - if st.oglobals.langFeatureNullness then - match nullness.Evaluate() with - | NullnessInfo.WithNull -> p_byteB 12 st - | NullnessInfo.WithoutNull -> p_byteB 13 st - | NullnessInfo.AmbivalentToNull -> p_byteB 14 st - + p_nullnessB 12 nullness st // B tags: 12=WithNull, 13=WithoutNull, 14=Ambivalent p_byte 2 st p_tcref "typ" tc st p_tys tinst st | TType_fun(d, r, nullness) -> - if st.oglobals.langFeatureNullness then - match nullness.Evaluate() with - | NullnessInfo.WithNull -> p_byteB 15 st - | NullnessInfo.WithoutNull -> p_byteB 16 st - | NullnessInfo.AmbivalentToNull -> p_byteB 17 st - + p_nullnessB 15 nullness st // B tags: 15=WithNull, 16=WithoutNull, 17=Ambivalent p_byte 3 st // Note, the "this" argument may be found in the domain position of a function type, so propagate the isStructThisArgPos value p_ty2 isStructThisArgPos d st p_ty r st | TType_var(r, nullness) -> - if st.oglobals.langFeatureNullness then - match nullness.Evaluate() with - | NullnessInfo.WithNull -> p_byteB 18 st - | NullnessInfo.WithoutNull -> p_byteB 19 st - | NullnessInfo.AmbivalentToNull -> p_byteB 20 st - + p_nullnessB 18 nullness st // B tags: 18=WithNull, 19=WithoutNull, 20=Ambivalent p_byte 4 st p_tpref r st diff --git a/tests/FSharp.Compiler.ComponentTests/ErrorMessages/DuplicateExtensionMemberTests.fs b/tests/FSharp.Compiler.ComponentTests/ErrorMessages/DuplicateExtensionMemberTests.fs index dd53e92951a..c0021c55ebc 100644 --- a/tests/FSharp.Compiler.ComponentTests/ErrorMessages/DuplicateExtensionMemberTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/ErrorMessages/DuplicateExtensionMemberTests.fs @@ -8,7 +8,10 @@ open FSharp.Test.Compiler module ``Duplicate Extension Members`` = [] - let ``Same type name from different namespaces should error``() = + let ``Same type name from different namespaces should error for static members``() = + // Static extension members on types with same simple name but different namespaces + // produce duplicate IL method signatures because the extended type's namespace is not + // encoded in the IL method name/signature for static extensions. FSharp """ namespace NS1 @@ -22,15 +25,40 @@ namespace NS3 module Extensions = type NS1.Task with - member x.Foo() = 1 + static member Foo() = 1 type NS2.Task with - member x.Bar() = 2 + static member Bar() = 2 """ |> typecheck |> shouldFail |> withDiagnosticMessageMatches "Extension members extending types with the same simple name 'Task'" + [] + let ``Same type name from different namespaces should be allowed for instance members``() = + // Instance extension members are safe because the extended type becomes the first + // parameter in IL, differentiating the signatures. + FSharp """ +namespace NS1 + +type Task = class end + +namespace NS2 + +type Task = class end + +namespace NS3 + +module Extensions = + type NS1.Task with + member x.Foo() = 1 + + type NS2.Task with + member x.Bar() = 2 + """ + |> typecheck + |> shouldSucceed + [] let ``Generic and non-generic types with same base name should be allowed``() = // This tests that Expr and Expr<'T> are allowed since they have different LogicalNames @@ -53,7 +81,8 @@ module Extensions = |> shouldSucceed [] - let ``Same generic type name from different namespaces should error``() = + let ``Same generic type name from different namespaces should error for static members``() = + // Same IL collision issue as non-generic, but with generic types. FSharp """ namespace NS1 @@ -67,15 +96,38 @@ namespace NS3 module Extensions = type NS1.Container<'T> with - member x.Foo() = 1 + static member Foo() = 1 type NS2.Container<'T> with - member x.Bar() = 2 + static member Bar() = 2 """ |> typecheck |> shouldFail |> withDiagnosticMessageMatches "Extension members extending types with the same simple name 'Container`1'" + [] + let ``Same generic type name from different namespaces should be allowed for instance members``() = + FSharp """ +namespace NS1 + +type Container<'T> = class end + +namespace NS2 + +type Container<'T> = class end + +namespace NS3 + +module Extensions = + type NS1.Container<'T> with + member x.Foo() = 1 + + type NS2.Container<'T> with + member x.Bar() = 2 + """ + |> typecheck + |> shouldSucceed + [] let ``Extensions on same type should be allowed``() = FSharp """ diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index f236ca6599d..63150ad3a7d 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -267,6 +267,7 @@ + diff --git a/tests/FSharp.Compiler.ComponentTests/Import/ImportTests.fs b/tests/FSharp.Compiler.ComponentTests/Import/ImportTests.fs index 8bd3625faf3..1ddccfd9dd5 100644 --- a/tests/FSharp.Compiler.ComponentTests/Import/ImportTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Import/ImportTests.fs @@ -679,6 +679,7 @@ let updateAge () = """ |> asLibrary |> withReferences [fsLib] + |> ignoreWarnings |> compile |> shouldSucceed |> ignore @@ -935,3 +936,111 @@ let v = new y() |> compile |> shouldSucceed |> ignore + + // Regression test: B-stream misalignment when pickling metadata with langFeatureNullness=false + // When a library is compiled with LangVersion 8.0 (no nullness), the type nullness B-stream bytes + // must still be written to keep the stream aligned with unconditional reads and constraint data. + // Without the fix, the reader hits NotSupportsNull constraint bytes (0x01) when expecting type + // nullness values, causing FS0229 "u_ty - 4/B". + [] + let ``Referencing library compiled with LangVersion 8 should not produce FS0229 B-stream error`` () = + let fsLib = + FSharp """ +module LibA + +type Result<'T, 'E> = + | Ok of 'T + | Error of 'E + +let bind (f: 'T -> Result<'U, 'E>) (r: Result<'T, 'E>) : Result<'U, 'E> = + match r with + | Ok x -> f x + | Error e -> Error e + +let map (f: 'T -> 'U) (r: Result<'T, 'E>) : Result<'U, 'E> = + bind (f >> Ok) r + +type Builder() = + member _.Return(x: 'T) : Result<'T, 'E> = Ok x + member _.ReturnFrom(x: Result<'T, 'E>) : Result<'T, 'E> = x + member _.Bind(m: Result<'T, 'E>, f: 'T -> Result<'U, 'E>) : Result<'U, 'E> = bind f m + +let builder = Builder() + +let inline combine<'T, 'U, 'E when 'T : equality and 'U : comparison> + (r1: Result<'T, 'E>) (r2: Result<'U, 'E>) : Result<'T * 'U, 'E> = + match r1, r2 with + | Ok t, Ok u -> Ok(t, u) + | Error e, _ | _, Error e -> Error e + """ + |> withName "LibA" + |> asLibrary + |> withLangVersion80 + + FSharp """ +module LibB + +open LibA + +let test() = + let r1 = Ok 42 + let r2 = Ok "hello" + combine r1 r2 + """ + |> asLibrary + |> withReferences [fsLib] + |> compile + |> shouldSucceed + |> ignore + + [] + let ``Referencing library with many generic types compiled at LangVersion 8 should not produce FS0229`` () = + let fsLib = + FSharp """ +module Lib + +type Wrapper<'T> = { Value: 'T } +type Pair<'T, 'U> = { First: 'T; Second: 'U } +type Triple<'T, 'U, 'V> = { A: 'T; B: 'U; C: 'V } + +let wrap (x: 'T) : Wrapper<'T> = { Value = x } +let pair (x: 'T) (y: 'U) : Pair<'T, 'U> = { First = x; Second = y } +let triple (x: 'T) (y: 'U) (z: 'V) : Triple<'T, 'U, 'V> = { A = x; B = y; C = z } + +let mapWrapper (f: 'T -> 'U) (w: Wrapper<'T>) : Wrapper<'U> = { Value = f w.Value } +let mapPair (f: 'T -> 'T2) (g: 'U -> 'U2) (p: Pair<'T, 'U>) : Pair<'T2, 'U2> = + { First = f p.First; Second = g p.Second } + +type ChainBuilder() = + member _.Return(x: 'T) : Wrapper<'T> = wrap x + member _.Bind(m: Wrapper<'T>, f: 'T -> Wrapper<'U>) : Wrapper<'U> = f m.Value + +let chain = ChainBuilder() + +let inline constrained<'T when 'T : equality> (x: 'T) (y: 'T) = x = y +let inline constrained2<'T, 'U when 'T : equality and 'U : comparison> (x: 'T) (y: 'U) = + (x, y) + """ + |> withName "GenericLib" + |> asLibrary + |> withLangVersion80 + + FSharp """ +module Consumer + +open Lib + +let test() = + let w = wrap 42 + let mapped = mapWrapper (fun x -> x + 1) w + let p = pair "hello" 42 + let t = triple 1 "two" 3.0 + let eq = constrained 1 1 + let c = chain { return 42 } + (mapped, p, t, eq, c) + """ + |> asLibrary + |> withReferences [fsLib] + |> compile + |> shouldSucceed + |> ignore diff --git a/tests/FSharp.Compiler.ComponentTests/Language/ExtensionMethodTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/ExtensionMethodTests.fs index 956362312f8..3eb711097c2 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/ExtensionMethodTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/ExtensionMethodTests.fs @@ -697,7 +697,9 @@ module CompiledExtensions = ] [] - let ``Instance extension members for types with same simple name should error`` () = + let ``Instance extension members for types with same simple name should succeed`` () = + // Instance extension members compile with the extended type as the first IL parameter, + // so they can never produce duplicate IL signatures even with same simple type name. Fsx """ module Compiled @@ -712,10 +714,7 @@ module CompiledExtensions = member _.InstanceExtension() = () """ |> compile - |> shouldFail - |> withDiagnostics [ - (Error 3356, Line 11, Col 18, Line 11, Col 35, "Extension members extending types with the same simple name 'Task' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules.") - ] + |> shouldSucceed [] let ``Extension members on generic types with same simple name should error`` () = @@ -806,3 +805,48 @@ module CompiledExtensions = (Error 3356, Line 12, Col 23, Line 12, Col 33, "Extension members extending types with the same simple name 'Task' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules.") (Error 3356, Line 13, Col 23, Line 13, Col 33, "Extension members extending types with the same simple name 'Task' but different fully qualified names cannot be defined in the same module. Consider defining these extensions in separate modules.") ] + + [] + let ``Instance inline extension members on builder types with same simple name should succeed`` () = + // Regression test for IcedTasks-like pattern: instance (inline) extension members on + // computation expression builder types with the same simple name from different namespaces. + // Instance extension members compile with the extended type as the first IL parameter, + // so signatures never collide even when the simple type name is the same. + Fsx + """ +namespace NsA +type BuilderBase() = class end + +namespace NsB +type BuilderBase() = class end + +namespace Extensions +module M = + type NsA.BuilderBase with + member inline this.Bind(x: int, f) = f x + member inline this.ReturnFrom(x: int) = x + + type NsB.BuilderBase with + member inline this.Source(x: int) = x + member inline this.Bind(x: string, f) = f x + """ + |> compile + |> shouldSucceed + + [] + let ``Mixed static and instance extension members - only static should error`` () = + Fsx + """ +module Compiled + +type Task = { F: int } + +module CompiledExtensions = + type System.Threading.Tasks.Task with + static member StaticExt() = () + + type Task with + member _.InstanceExt() = () + """ + |> compile + |> shouldSucceed