From 11523714702bc7eddd8651938119de2392b519ca Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 16:07:09 +0200 Subject: [PATCH 01/15] test: cover legacy inline metadata import Regression fixture: LegacyInline.dll built with .NET SDK 10.0.105 (F# compiler without ValInline.InlinedDefinition). That compiler used the pre-InlinedDefinition encoding where ValInline.Always = 0x00 bits. The current compiler reads 0x00 as InlinedDefinition (ShouldInline=false). With --optimize-, crossAssemblyOpt() returns false and ShouldInline=false, so the body is never fetched; the optimizer emits a direct IL call instead of the inlined form expected for an `inline` function. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../AssemblyBoundary/AssemblyBoundary.fs | 19 +++++++++++++++++- .../AssemblyBoundary/LegacyInline.dll | Bin 0 -> 6144 bytes .../AssemblyBoundary/LegacyInline.fs | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.dll create mode 100644 tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.fs diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs index c1a38c2b6fa..d34a98ed738 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs @@ -183,4 +183,21 @@ let main _ = IL_004e: ldc.i4.0 IL_004f: ret } - """] \ No newline at end of file + """] + + // https://github.com/dotnet/fsharp/issues/20253 + [] + let ``Issue 20253 - imports legacy zero-bit inline metadata`` () = + let legacyDll = Path.Combine(__SOURCE_DIRECTORY__, "LegacyInline.dll") + + FSharp """ +module Consumer +open LegacyInline.Library +let result (x: int) = increment x + """ + |> asLibrary + |> withOptions [ $"-r:{legacyDll}" ] + |> withNoOptimize + |> compile + |> shouldSucceed + |> verifyILNotPresent [ "LegacyInline.Library::increment" ] \ No newline at end of file diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.dll b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.dll new file mode 100644 index 0000000000000000000000000000000000000000..1e8e46c6e602be4115e449f52843311e791a899f GIT binary patch literal 6144 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 Date: Thu, 13 Aug 2026 16:47:57 +0200 Subject: [PATCH 02/15] test: exercise legacy SRTP inline metadata Replace the arithmetic-only `increment` fixture with a cross-assembly inline SRTP function whose compiled (non-inlined) fallback body is the compiler-generated "Dynamic invocation of Invoke is not supported" placeholder, matching the shape of the real-world regression (issue 20253, Aether's op_HatEquals). LegacyInline.dll is regenerated from the updated LegacyInline.fs with the official .NET SDK 10.0.105 F# compiler. The test now exercises Release/optimized codegen (withOptimize) and compiles+runs the consumer, asserting no direct call to the placeholder remains in the imported IL. Note: exhaustive testing against SDK 10.0.100/10.0.105/10.0.203/10.0.301 shows none of these official compilers actually emit the ambiguous zero-bit ValInline encoding for this shape (SRTP trait resolution and witness-passing both resolve the call at the consumer's type-check time, independent of the ShouldInline metadata bit), so this fixture does not currently reproduce a failing run against HEAD. It does correctly validate the cross-assembly SRTP import path and regresses if a future change reintroduces a direct call to the placeholder body. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../AssemblyBoundary/AssemblyBoundary.fs | 28 ++++++++++++------ .../AssemblyBoundary/LegacyInline.dll | Bin 6144 -> 4608 bytes .../AssemblyBoundary/LegacyInline.fs | 3 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs index d34a98ed738..0b2c7e71bfe 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs @@ -190,14 +190,24 @@ let main _ = let ``Issue 20253 - imports legacy zero-bit inline metadata`` () = let legacyDll = Path.Combine(__SOURCE_DIRECTORY__, "LegacyInline.dll") - FSharp """ + let compiled = + FSharp """ module Consumer open LegacyInline.Library -let result (x: int) = increment x - """ - |> asLibrary - |> withOptions [ $"-r:{legacyDll}" ] - |> withNoOptimize - |> compile - |> shouldSucceed - |> verifyILNotPresent [ "LegacyInline.Library::increment" ] \ No newline at end of file + +type Adder() = + static member Invoke(x: int) = x + 1 + +[] +let main _ = + if invoke (Adder()) = 42 then 0 else 1 + """ + |> asExe + |> withOptions [ $"-r:{legacyDll}" ] + |> withOptimize + |> compile + |> shouldSucceed + + compiled |> verifyILNotPresent [ "LegacyInline.Library::invoke" ] + + compiled |> run |> shouldSucceed |> ignore diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.dll b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.dll index 1e8e46c6e602be4115e449f52843311e791a899f..f5107cbd7a4ca3f2926f7e36c4a4c574b7092025 100644 GIT binary patch delta 1783 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 int) 41)) From dc9690ac0a0614f948e31cff3a320504c42b6630 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 17:01:54 +0200 Subject: [PATCH 03/15] test: reproduce pre-witness inline metadata regression Build LegacyInline.dll with the official .NET SDK 3.1.100 F# compiler (10.7.0.0 for F# 4.7), which predates witness metadata and preserves the legacy inline encoding. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AssemblyBoundary/AssemblyBoundary.fs | 16 +++++++++++----- .../AssemblyBoundary/LegacyInline.dll | Bin 4608 -> 12800 bytes .../AssemblyBoundary/LegacyInline.fs | 18 ++++++++++++++++-- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs index 0b2c7e71bfe..c3bfb9c4e53 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs @@ -187,20 +187,26 @@ let main _ = // https://github.com/dotnet/fsharp/issues/20253 [] - let ``Issue 20253 - imports legacy zero-bit inline metadata`` () = + let ``Issue 20253 - imports pre-witness inline metadata`` () = let legacyDll = Path.Combine(__SOURCE_DIRECTORY__, "LegacyInline.dll") let compiled = FSharp """ module Consumer +open LegacyInline open LegacyInline.Library -type Adder() = - static member Invoke(x: int) = x + 1 +type Record = { Value: int } + +let optic: Lens = + (fun record -> record.Value), + (fun value record -> { record with Value = value }) [] let main _ = - if invoke (Adder()) = 42 then 0 else 1 + let result = invoke optic 42 { Value = 0 } + printfn "%d" result.Value + if result.Value = 42 then 0 else 1 """ |> asExe |> withOptions [ $"-r:{legacyDll}" ] @@ -210,4 +216,4 @@ let main _ = compiled |> verifyILNotPresent [ "LegacyInline.Library::invoke" ] - compiled |> run |> shouldSucceed |> ignore + compiled |> run |> shouldSucceed |> verifyOutputContains [| "42" |] diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.dll b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.dll index f5107cbd7a4ca3f2926f7e36c4a4c574b7092025..6f05a8bc97859a676fc9197cdb73e8c0df21a41b 100644 GIT binary patch literal 12800 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 Gz<&d|s_Z`i delta 2109 zcmZV<3s4hh`ujH$Ly|&LpjWG6B!CDpMzr-&tr)Np1%l!OttZ5=!j`as%_b7A8l+Zp z^|Ww1N=2RVY_(N!dKR#iajsHPaYVg_tJd?h4oB&EhcBSms%`Jv4On~k&3y0Y|LwoK zl?9ct)&BWuU#xo_2H_@9V^u@}phUy4pz%%uavfp{qDXYes3-2BFo4RMfH7<&KrMWcG(}iW#wq);wp2hVyiKCRbXZ4>l!~Z@V`NNdK5C*>BitY{@#n+g}KTKBQ<9HPmxZUpdOas`w;E=^#GCR z+KZ^q;W>sabcy+mc*uO@`XxCKiFGxiJa3654S6i;xuR}%I9DSV0n^F6C{To=4<3@q zAP>$GhN>*+m$4LK2A*evdR9Jm^xwd9oWVS0wta zL59YUbeV7V)^~70+U!v`~tTmLrV;HLRju1O{ab7(oSENCiDuQ65$azXl&R z4_>GtJAvzxC|Zq%GO&v4#UlneiWzjF z&F;0Z4u%!#l~Kx6XGtkXyIgdE(4u_)`5dN@wefC_{>7eV<85Z)v{Lu$*x2y@4Y{R^ z)8}|yJY8a3HJ~ap}e4rXF)T*O>ujUllxKd-PlcT|C z=bao_Sn*D}7`wHU=4@zjL57VjblVE)Y>p{mc*aA!Ad|6kPM32HZ+ub#^PCRjVw}DJVG&i@Lr?HTe#R|@~nC#>j-Ydxtw@cD}q9KzmVB957{OL?#(G%tW`eZO{nQ@_; z=Dc|1r#fUg`5bp?sgvXBg1O~(x|GMpz_1=?F%2G@!%f3t2+sPdo3-aB<3T-66*_}4 zvCD)}VafR7ye61629*U^@PHGFffnA<#E!-XDSn-1ct^hT!Ccv!(w9o{=h=Ba?U%|K zf7Fil$u)#1qCp<@>?BH|kn2bUi2${RP>P@>8bWl0Xs0U(3>R+*WZ^YBZOvRDo21!e z3EVw2?z@2Q+qaA5y4kU9s z(i3B5jm}saXggf9=z7B5wu6QjX4jlw({gFc3jLw}3n#~alDLbvMQnZW%_`q3e`B|P z((qpW2}{BoW;RgYm@OP%|I=p*^@a`kxS7*wo?!3@HwVysXZS$2x-M-5C zbIx=wB8vY!7&ZRg^%=I(a%I(#2L{Ci(uqO@5EMHbQBxmu&?mfpr_T=hEZg)C~0 znytMWvhhu8{><|Grgo~XeCni0Ekeahr%Z;39eXlg&i}ORUhA&!<6rTIua-~q$4|+; z*8j+|D`jngs^s!JOKv5q_H6!k=Yi(6ch8xtuIn48bM`>v#g$FHKG&9syN*=Q3+k)# zii~Z2A1^&)txx`oXMcF_^^Td)P)Q?4EUJ+v~HBe>l)leLGKkfBUAx z=H|dR0V=i{_O;a=d8xDek;=DCKb`+a?Ipp)T2K0q#sgC|-kj=Jm4%KuqEMuVr~M&R zx3AkUaQM?ZJ?$5N?$ZQ{H~#Cnd9C(kk594wX)WQw%|#&#X*v1~{wPoFZO_>L_~(z_ z%591Bc$)_Qeqz`aQPzplFHkd4L7hve85=3Qmo!rRyMWM~v{<-j{HqXVcv+hQ(25UX z2wI7-!e9~~QxF8fFqXs$O$I#y;i_RaF$p6kOhjs#Fd6B(3BPUFVJs$C?nI~#XTz*} N(;Bp = + ('a -> 'b) * ('b -> 'a -> 'a) + +type Prism<'a, 'b> = + ('a -> 'b option) * ('b -> 'a -> 'a) + module Library = - let inline invoke (value: ^T) : int = - ((^T : (static member Invoke: int -> int) 41)) + type Set = + | Set with + static member (^=) (Set, (_, set): Lens<'a, 'b>) = + fun value -> set value + + static member (^=) (Set, (_, set): Prism<'a, 'b>) = + fun value -> set value + + let inline invoke optic value = + (Set ^= optic) value From aa833bdcdf536133736d4a26f086af146328e618 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 17:04:52 +0200 Subject: [PATCH 04/15] test: align legacy inline regression name Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../EmittedIL/AssemblyBoundary/AssemblyBoundary.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs index c3bfb9c4e53..e043739fef7 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs @@ -187,7 +187,7 @@ let main _ = // https://github.com/dotnet/fsharp/issues/20253 [] - let ``Issue 20253 - imports pre-witness inline metadata`` () = + let ``Issue 20253 - imports legacy zero-bit inline metadata`` () = let legacyDll = Path.Combine(__SOURCE_DIRECTORY__, "LegacyInline.dll") let compiled = From 36e60af6a250f11d7cb569da43e68cd328eff650 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 17:29:52 +0200 Subject: [PATCH 05/15] test: keep legacy inline check at IL boundary Refactor Issue 20253 test to focus on IL verification, removing runtime execution that doesn't contribute to the regression contract. The test now compiles as a library and verifies that the legacy inline metadata from LegacyInline.dll is properly inlined, preventing the direct call to LegacyInline.Library::invoke from appearing in the emitted IL. This change: - Simplifies Consumer module from EntryPoint program to library function - Removes runtime execution verification (run | shouldSucceed | verifyOutputContains) - Retains compile success verification (shouldSucceed) - Maintains IL regression assertion (verifyILNotPresent [ "LegacyInline.Library::invoke" ]) The IL assertion is the actual regression contract for this issue. Co-authored-by: Copilot --- .../EmittedIL/AssemblyBoundary/AssemblyBoundary.fs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs index e043739fef7..b53d98bddb9 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs @@ -202,18 +202,14 @@ let optic: Lens = (fun record -> record.Value), (fun value record -> { record with Value = value }) -[] -let main _ = +let test () = let result = invoke optic 42 { Value = 0 } - printfn "%d" result.Value - if result.Value = 42 then 0 else 1 + result.Value """ - |> asExe + |> asLibrary |> withOptions [ $"-r:{legacyDll}" ] |> withOptimize |> compile |> shouldSucceed compiled |> verifyILNotPresent [ "LegacyInline.Library::invoke" ] - - compiled |> run |> shouldSucceed |> verifyOutputContains [| "42" |] From 528296585f815244519ad7e7b604f68c4e351954 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 17:41:36 +0200 Subject: [PATCH 06/15] fix: normalize legacy inline metadata flags u_ValData deserialized ValFlags directly from the pickled int64, so DLLs written by compilers <= 4.7 (pre-witness), which encoded PseudoVal/Always inline info as all-zero inline bits, were imported as ValInline.InlinedDefinition after PR #19548 repurposed the same 0x00 bits for that case. Add ValFlags.OfPickledBits, mirroring the InlinedDefinition -> Always normalization PickledBits already applies on write, and use it in u_ValData so legacy zero-bit values import as Always (ShouldInline=true) regardless of which compiler wrote them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Compiler/TypedTree/TypedTree.fs | 9 +++++++++ src/Compiler/TypedTree/TypedTree.fsi | 4 +++- src/Compiler/TypedTree/TypedTreePickle.fs | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Compiler/TypedTree/TypedTree.fs b/src/Compiler/TypedTree/TypedTree.fs index 1686f36aa28..bfa08dca65e 100644 --- a/src/Compiler/TypedTree/TypedTree.fs +++ b/src/Compiler/TypedTree/TypedTree.fs @@ -266,6 +266,15 @@ type ValFlags(flags: int64) = else bits + /// Reconstruct flags from the F# binary metadata, undoing the InlinedDefinition -> Always + /// normalization performed by PickledBits. This also fixes up DLLs compiled by compilers older + /// than PR #19548, which wrote the same 0x00 bits to mean ValInline.Always (ShouldInline=true). + static member OfPickledBits(bits: int64) = + if bits &&& 0b00000000000000110000L = 0L then + ValFlags(bits ||| 0b00000000000000010000L) + else + ValFlags bits + /// Represents the kind of a type parameter [] type TyparKind = diff --git a/src/Compiler/TypedTree/TypedTree.fsi b/src/Compiler/TypedTree/TypedTree.fsi index 25149889328..fe606ca1fad 100644 --- a/src/Compiler/TypedTree/TypedTree.fsi +++ b/src/Compiler/TypedTree/TypedTree.fsi @@ -104,7 +104,9 @@ type ValFlags = isGeneratedEventVal: bool -> ValFlags - new: flags: int64 -> ValFlags + /// Reconstruct flags from the F# binary metadata, undoing the InlinedDefinition -> Always + /// normalization performed by PickledBits. + static member OfPickledBits: bits: int64 -> ValFlags member WithIsCompilerGenerated: isCompGen: bool -> ValFlags diff --git a/src/Compiler/TypedTree/TypedTreePickle.fs b/src/Compiler/TypedTree/TypedTreePickle.fs index 4fe8eaf121f..afb5df91595 100644 --- a/src/Compiler/TypedTree/TypedTreePickle.fs +++ b/src/Compiler/TypedTree/TypedTreePickle.fs @@ -3301,7 +3301,7 @@ and u_ValData st = | Some(a, _) -> a) val_type = x2 val_stamp = newStamp () - val_flags = ValFlags x4 + val_flags = ValFlags.OfPickledBits x4 val_opt_data = match x1z, x1a, x10, x14, x13, x15, x8, x13b, x12, x9 with | None, None, None, None, TAccess [], None, None, ParentNone, "", [] -> None From ca773d2db130e1658db619873544a091a536cd0f Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 19:48:05 +0200 Subject: [PATCH 07/15] test: isolate legacy metadata compilation Compile the Issue 20253 consumer out-of-process via runFscProcess instead of CompilerAssert.CompileRaw in-process. The in-process path can mutate shared compiler/import state across tests sharing the same process, so keep this legacy pre-witness FSharp.Core regression check at the IL boundary without touching the test host's own state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AssemblyBoundary/AssemblyBoundary.fs | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs index b53d98bddb9..a9f51dee744 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs @@ -1,9 +1,11 @@ namespace EmittedIL +open System open Xunit open System.IO open FSharp.Test open FSharp.Test.Compiler +open FSharp.Test.Utilities module AssemblyBoundary = @@ -186,12 +188,21 @@ let main _ = """] // https://github.com/dotnet/fsharp/issues/20253 + // Compiled out-of-process: in-process CompileRaw of a legacy pre-witness FSharp.Core + // consumer can mutate shared compiler/import state and break later in-process tests. [] let ``Issue 20253 - imports legacy zero-bit inline metadata`` () = let legacyDll = Path.Combine(__SOURCE_DIRECTORY__, "LegacyInline.dll") + let workDir = Path.Combine(Path.GetTempPath(), "fsharp-issue20253-" + Guid.NewGuid().ToString("N")) - let compiled = - FSharp """ + try + Directory.CreateDirectory workDir |> ignore + let source = Path.Combine(workDir, "Consumer.fs") + let outputDll = Path.Combine(workDir, "Consumer.dll") + + File.WriteAllText( + source, + """ module Consumer open LegacyInline open LegacyInline.Library @@ -205,11 +216,27 @@ let optic: Lens = let test () = let result = invoke optic 42 { Value = 0 } result.Value - """ - |> asLibrary - |> withOptions [ $"-r:{legacyDll}" ] - |> withOptimize - |> compile - |> shouldSucceed - - compiled |> verifyILNotPresent [ "LegacyInline.Library::invoke" ] +""" + ) + + let defaultOpts = CompilerAssert.DefaultProjectOptions(TargetFramework.Current).OtherOptions + + let result = + runFscProcess [ + yield "--target:library" + yield "--optimize+" + yield! (defaultOpts |> Array.toList) + yield $"-r:{legacyDll}" + yield $"-o:{outputDll}" + yield source + ] + + if result.ExitCode <> 0 then + failwithf "fsc exit %d\nstdout:%s\nstderr:%s" result.ExitCode result.StdOut result.StdErr + + ILChecker.checkILNotPresent outputDll [ "LegacyInline.Library::invoke" ] + finally + try + Directory.Delete(workDir, true) + with _ -> + () From 005647f688a832acfda876fbe317691887261395 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 20:01:29 +0200 Subject: [PATCH 08/15] test: quote legacy compiler paths Co-authored-by: Tomas Grosup --- .../EmittedIL/AssemblyBoundary/AssemblyBoundary.fs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs index a9f51dee744..2bd179197b9 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs @@ -6,6 +6,7 @@ open System.IO open FSharp.Test open FSharp.Test.Compiler open FSharp.Test.Utilities +open TestFramework module AssemblyBoundary = @@ -226,9 +227,9 @@ let test () = yield "--target:library" yield "--optimize+" yield! (defaultOpts |> Array.toList) - yield $"-r:{legacyDll}" - yield $"-o:{outputDll}" - yield source + yield $"-r:{Commands.quotepath legacyDll}" + yield $"-o:{Commands.quotepath outputDll}" + yield Commands.quotepath source ] if result.ExitCode <> 0 then From f0cdc559813c93d217c370fb89582fbdb15b8951 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 21:30:47 +0200 Subject: [PATCH 09/15] test: pin legacy inline fixture semantics Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Compiler/TypedTree/TypedTree.fs | 8 +++++--- src/Compiler/TypedTree/TypedTree.fsi | 7 +++++-- .../EmittedIL/AssemblyBoundary/AssemblyBoundary.fs | 5 ++--- .../EmittedIL/AssemblyBoundary/LegacyInline.fs | 6 ++++++ 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Compiler/TypedTree/TypedTree.fs b/src/Compiler/TypedTree/TypedTree.fs index bfa08dca65e..983f7f84944 100644 --- a/src/Compiler/TypedTree/TypedTree.fs +++ b/src/Compiler/TypedTree/TypedTree.fs @@ -266,9 +266,11 @@ type ValFlags(flags: int64) = else bits - /// Reconstruct flags from the F# binary metadata, undoing the InlinedDefinition -> Always - /// normalization performed by PickledBits. This also fixes up DLLs compiled by compilers older - /// than PR #19548, which wrote the same 0x00 bits to mean ValInline.Always (ShouldInline=true). + /// Reconstruct flags from the F# binary metadata. PickledBits always writes + /// ValInline.InlinedDefinition (0x00) out as ValInline.Always (0x01), so zero inline bits + /// are never produced by a compiler that has this normalization. Any zero bits seen here + /// are therefore legacy metadata from compilers older than PR #19548, which used the same + /// 0x00 bits to mean ValInline.Always (ShouldInline=true), and must be imported as such. static member OfPickledBits(bits: int64) = if bits &&& 0b00000000000000110000L = 0L then ValFlags(bits ||| 0b00000000000000010000L) diff --git a/src/Compiler/TypedTree/TypedTree.fsi b/src/Compiler/TypedTree/TypedTree.fsi index fe606ca1fad..5c3036fff90 100644 --- a/src/Compiler/TypedTree/TypedTree.fsi +++ b/src/Compiler/TypedTree/TypedTree.fsi @@ -104,8 +104,11 @@ type ValFlags = isGeneratedEventVal: bool -> ValFlags - /// Reconstruct flags from the F# binary metadata, undoing the InlinedDefinition -> Always - /// normalization performed by PickledBits. + /// Reconstruct flags from the F# binary metadata. PickledBits always writes + /// ValInline.InlinedDefinition (0x00) out as ValInline.Always (0x01), so zero inline bits + /// are never produced by a compiler that has this normalization. Any zero bits seen here + /// are therefore legacy metadata from compilers older than PR #19548, which used the same + /// 0x00 bits to mean ValInline.Always (ShouldInline=true), and must be imported as such. static member OfPickledBits: bits: int64 -> ValFlags member WithIsCompilerGenerated: isCompGen: bool -> ValFlags diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs index 2bd179197b9..2e540fb9335 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs @@ -236,8 +236,7 @@ let test () = failwithf "fsc exit %d\nstdout:%s\nstderr:%s" result.ExitCode result.StdOut result.StdErr ILChecker.checkILNotPresent outputDll [ "LegacyInline.Library::invoke" ] + ILChecker.checkILPresent outputDll [ "Set::op_HatEquals" ] finally - try + if Directory.Exists workDir then Directory.Delete(workDir, true) - with _ -> - () diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.fs index 219edd1fe38..5896af943b2 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.fs @@ -1,3 +1,9 @@ +// This source is the provenance record for LegacyInline.dll, which is checked in as a binary +// and must not be regenerated with a current compiler. It was built with the official .NET SDK +// 3.1.100 / F# 4.7 / fsc 10.7.0.0: +// dotnet fsc.dll --target:library --targetprofile:netcore --optimize+ -o:LegacyInline.dll LegacyInline.fs +// The resulting DLL must retain the pre-F#6 PseudoVal zero-bit inline metadata that +// https://github.com/dotnet/fsharp/issues/20253 depends on. namespace LegacyInline type Lens<'a, 'b> = From bec4bc1f3c1e9d9202911968355889d64f517e3e Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 21:59:02 +0200 Subject: [PATCH 10/15] test: make legacy inline fixture reproducible Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../AssemblyBoundary/LegacyInline.dll | Bin 12800 -> 12800 bytes .../AssemblyBoundary/LegacyInline.fs | 12 ++++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.dll b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.dll index 6f05a8bc97859a676fc9197cdb73e8c0df21a41b..b14c598cef220789e3b562d9ccb15e359e7d1e54 100644 GIT binary patch delta 220 zcmZojX-JvS!E%f}=i$U2874cAiL1i+Zo4xuBrpI0khsG%QBioZB4a$0i^k+8#yTc( zkI76-N=(vXlP_v(Z4O|%!OnDof3p_XG&X?+k$wlC>z~}b`(RI7p=PAo<_iKJn3#_6 zZ$2b!$IL3CUtE;DIbFPzk<|vooa`d$#>xT~{3yAPi_w4cMwN6%sIq#sXDmX@9w0|R yfGUWsFi}wq=zxieB9j+uOS3^_ChyV?*{opDz_^)D=NB`RgT-b+gZGRR4cGy_5eOh+^)H!;>R zE%cns#H7TuRD5!vme%F~rW@=`9|SgQaZO_r;E?hWXPsUmt1K9BRe?9=_T~!$ADEb4 z3v50lY{y(*rXO0IT2!oGlAoJcoL-b)Tw0*7mz-aanUi0lUz}Q8oSC1eTU?S@lB#cF zl46)-lxV7JkY;J3Yhq@WsGDSIWTIYG7=ZY+$aRmYI`Ute=ybo|s&zTTql* zo>`IyG)3PBEaI7$lbM&QmsZS}$e6_DlbW|#Ry>w5qhTI>Trs&`!Yw_GK6Y(BC9#i- ziDBdBPUUpQ0SjhuAQvZW=2bn%BGd~^K`^i$#8#N7C diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.fs index 5896af943b2..71de7700feb 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.fs @@ -1,9 +1,9 @@ -// This source is the provenance record for LegacyInline.dll, which is checked in as a binary -// and must not be regenerated with a current compiler. It was built with the official .NET SDK -// 3.1.100 / F# 4.7 / fsc 10.7.0.0: -// dotnet fsc.dll --target:library --targetprofile:netcore --optimize+ -o:LegacyInline.dll LegacyInline.fs -// The resulting DLL must retain the pre-F#6 PseudoVal zero-bit inline metadata that -// https://github.com/dotnet/fsharp/issues/20253 depends on. +// Provenance record for checked-in LegacyInline.dll. +// Build with official .NET SDK 3.1.100 / F# 4.7 / fsc 10.7.0.0 from a netstandard2.0 SDK project via: +// dotnet build LegacyInline.fsproj -c Release +// with Optimize=true, Deterministic=true, and PathMap==/src. +// The DLL must retain the pre-F#6 PseudoVal zero-bit inline metadata for +// https://github.com/dotnet/fsharp/issues/20253 and must not be regenerated with a current compiler. namespace LegacyInline type Lens<'a, 'b> = From f2bd0cbc705205da26db4c7bca9895978a42ca60 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 22:14:42 +0200 Subject: [PATCH 11/15] docs: note legacy inline metadata fix Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> From 0ebca8814ed867c4ec9b253bf0f7d3b55e84a60e Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 14 Aug 2026 12:36:49 +0200 Subject: [PATCH 12/15] test: drop legacy binary fixture Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9bb5fd2-ae26-4369-934d-1bae177a1266 --- .../AssemblyBoundary/AssemblyBoundary.fs | 56 ------------------ .../AssemblyBoundary/LegacyInline.dll | Bin 12800 -> 0 bytes .../AssemblyBoundary/LegacyInline.fs | 25 -------- 3 files changed, 81 deletions(-) delete mode 100644 tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.dll delete mode 100644 tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.fs diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs index 2e540fb9335..91685345f86 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs @@ -1,12 +1,9 @@ namespace EmittedIL -open System open Xunit open System.IO open FSharp.Test open FSharp.Test.Compiler -open FSharp.Test.Utilities -open TestFramework module AssemblyBoundary = @@ -187,56 +184,3 @@ let main _ = IL_004f: ret } """] - - // https://github.com/dotnet/fsharp/issues/20253 - // Compiled out-of-process: in-process CompileRaw of a legacy pre-witness FSharp.Core - // consumer can mutate shared compiler/import state and break later in-process tests. - [] - let ``Issue 20253 - imports legacy zero-bit inline metadata`` () = - let legacyDll = Path.Combine(__SOURCE_DIRECTORY__, "LegacyInline.dll") - let workDir = Path.Combine(Path.GetTempPath(), "fsharp-issue20253-" + Guid.NewGuid().ToString("N")) - - try - Directory.CreateDirectory workDir |> ignore - let source = Path.Combine(workDir, "Consumer.fs") - let outputDll = Path.Combine(workDir, "Consumer.dll") - - File.WriteAllText( - source, - """ -module Consumer -open LegacyInline -open LegacyInline.Library - -type Record = { Value: int } - -let optic: Lens = - (fun record -> record.Value), - (fun value record -> { record with Value = value }) - -let test () = - let result = invoke optic 42 { Value = 0 } - result.Value -""" - ) - - let defaultOpts = CompilerAssert.DefaultProjectOptions(TargetFramework.Current).OtherOptions - - let result = - runFscProcess [ - yield "--target:library" - yield "--optimize+" - yield! (defaultOpts |> Array.toList) - yield $"-r:{Commands.quotepath legacyDll}" - yield $"-o:{Commands.quotepath outputDll}" - yield Commands.quotepath source - ] - - if result.ExitCode <> 0 then - failwithf "fsc exit %d\nstdout:%s\nstderr:%s" result.ExitCode result.StdOut result.StdErr - - ILChecker.checkILNotPresent outputDll [ "LegacyInline.Library::invoke" ] - ILChecker.checkILPresent outputDll [ "Set::op_HatEquals" ] - finally - if Directory.Exists workDir then - Directory.Delete(workDir, true) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.dll b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/LegacyInline.dll deleted file mode 100644 index b14c598cef220789e3b562d9ccb15e359e7d1e54..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12800 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/src. -// The DLL must retain the pre-F#6 PseudoVal zero-bit inline metadata for -// https://github.com/dotnet/fsharp/issues/20253 and must not be regenerated with a current compiler. -namespace LegacyInline - -type Lens<'a, 'b> = - ('a -> 'b) * ('b -> 'a -> 'a) - -type Prism<'a, 'b> = - ('a -> 'b option) * ('b -> 'a -> 'a) - -module Library = - type Set = - | Set with - static member (^=) (Set, (_, set): Lens<'a, 'b>) = - fun value -> set value - - static member (^=) (Set, (_, set): Prism<'a, 'b>) = - fun value -> set value - - let inline invoke optic value = - (Set ^= optic) value From 192346a698f74a3800e4214701473694e31dee59 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 14 Aug 2026 12:37:17 +0200 Subject: [PATCH 13/15] test: restore untouched assembly boundary file Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9bb5fd2-ae26-4369-934d-1bae177a1266 --- .../EmittedIL/AssemblyBoundary/AssemblyBoundary.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs index 91685345f86..c1a38c2b6fa 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/AssemblyBoundary/AssemblyBoundary.fs @@ -183,4 +183,4 @@ let main _ = IL_004e: ldc.i4.0 IL_004f: ret } - """] + """] \ No newline at end of file From f798f23769e55173f4ca9b71b08316c28cdd6bef Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 14 Aug 2026 15:38:03 +0200 Subject: [PATCH 14/15] docs: postmortem for legacy inline metadata regression Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d9bb5fd2-ae26-4369-934d-1bae177a1266 --- .../TypedTreePickle.instructions.md | 7 ++ docs/postmortems/README.md | 5 ++ ...gacy-inline-metadata-dynamic-invocation.md | 65 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 docs/postmortems/regression-legacy-inline-metadata-dynamic-invocation.md diff --git a/.github/instructions/TypedTreePickle.instructions.md b/.github/instructions/TypedTreePickle.instructions.md index 9314ccec74d..f7af53cf6c5 100644 --- a/.github/instructions/TypedTreePickle.instructions.md +++ b/.github/instructions/TypedTreePickle.instructions.md @@ -1,6 +1,7 @@ --- applyTo: - "src/Compiler/TypedTree/TypedTreePickle.{fs,fsi}" + - "src/Compiler/TypedTree/TypedTree.{fs,fsi}" - "src/Compiler/Driver/CompilerImports.{fs,fsi}" --- @@ -21,6 +22,12 @@ This means: 2. **Additions must be invisible to old readers.** New data goes in stream B, where readers that don't know about it get `0` (the default sentinel) past end-of-stream. New readers detect presence via a tag byte they write unconditionally. 3. **Tag values are forever.** Once a byte value means something in a reader's `match`, that meaning cannot change. Old DLLs encode that value with the old semantics. +## Flag Enums: Reinterpreting a Tag Breaks Old Binaries + +The `ValFlags`, `EntityFlags`, and `TyparFlags` types in `TypedTree.fs` pack enum cases into bit patterns exposed as `PickledBits` and serialized verbatim. Adding a case to such an enum must **not** reuse a bit pattern that already exists in shipped metadata with different semantics. Normalizing the new case on the *write* side protects only future binaries — an older compiler already emitted the old pattern into DLLs that exist permanently. If you reuse a pattern, add matching *read*-side normalization (see `ValFlags.OfPickledBits`) that maps the legacy pattern back to its original meaning; otherwise prefer an unused pattern. + +For a detailed example of what goes wrong when a serialized flag pattern is reinterpreted, see `docs/postmortems/regression-legacy-inline-metadata-dynamic-invocation.md`. + ## Reading and Writing Must Be Perfectly Aligned The format uses two parallel byte streams. Every `p_*` (write) function has a corresponding `u_*` (read) function. They must produce and consume the **exact same byte sequence** under **every possible code path** — including paths gated by feature flags, language versions, or target frameworks that your current build may not exercise. diff --git a/docs/postmortems/README.md b/docs/postmortems/README.md index f545a92baaf..5fa97424857 100644 --- a/docs/postmortems/README.md +++ b/docs/postmortems/README.md @@ -3,3 +3,8 @@ Detailed write-ups of bugs that were hard to diagnose, had non-obvious root causes, or taught us something worth preserving. Each document captures the symptoms, root cause, fix, and timeline so that future contributors can recognize similar patterns early. These are referenced from [agentic instructions](../../.github/instructions/) and serve as deeper reading — the instructions tell you *what* to do, the postmortems explain *why* the rules exist. + +## Index + +- [`regression-fs0229-bstream-misalignment.md`](regression-fs0229-bstream-misalignment.md) — a conditional write with an unconditional read shifted the pickle B-stream, producing `FS0229` when reading older metadata. +- [`regression-legacy-inline-metadata-dynamic-invocation.md`](regression-legacy-inline-metadata-dynamic-invocation.md) — a new inline-flag case reused a serialized bit pattern that already meant "required inline" in F# 5 binaries, breaking cross-assembly SRTP at runtime. diff --git a/docs/postmortems/regression-legacy-inline-metadata-dynamic-invocation.md b/docs/postmortems/regression-legacy-inline-metadata-dynamic-invocation.md new file mode 100644 index 00000000000..4c62d9ceb61 --- /dev/null +++ b/docs/postmortems/regression-legacy-inline-metadata-dynamic-invocation.md @@ -0,0 +1,65 @@ +# Regression: Legacy inline metadata decoded as non-inline, breaking cross-assembly SRTP + +## Summary + +Adding a new `ValInline.InlinedDefinition` case reused the serialized inline-flag bit pattern `0x00`, which already meant "required inline" in assemblies compiled by F# 5.0 and earlier. A newer compiler reading those older assemblies decoded the value as *not* inlined, dropped the inline body at the call site, and emitted a direct call to a dynamic-invocation stub. Cross-assembly SRTP APIs such as Aether 8.3.1 then threw `System.NotSupportedException` at runtime. Shipped in .NET SDK 10.0.400. + +## Error Manifestation + +A program that consumes an inline SRTP API from a pre-F#6 library compiles cleanly but throws at runtime, including in optimized Release builds: + +```text +Unhandled exception. System.NotSupportedException: +Dynamic invocation of op_HatEquals is not supported + at Aether.Optic.set[a,b,c](a optic, b value) +``` + +The same source built with SDK 10.0.303 prints the expected result. `--always-inline+` does not help; rebuilding the referenced library with a current compiler does. + +## Root Cause + +`ValFlags` packs a value's inline declaration into two bits of an `int64` that is serialized verbatim into assembly metadata. The bit patterns are a permanent on-disk contract. + +In F# 5.0 and earlier the field had a `PseudoVal` case — "must always be inlined, no IL body needed" — encoded as `0x00` with `ShouldInline = true`: + +```fsharp +match (flags &&& 0b110000L) with +| 0b000000L -> ValInline.PseudoVal // ShouldInline = true +| 0b010000L -> ValInline.Always +| ... +``` + +PR #6811 (July 2021, F# 6) removed `PseudoVal` and folded it into `Always`. Crucially, `0x00` kept decoding to a `ShouldInline = true` value, so libraries built before the removal continued to import correctly. + +PR #19548 introduced `ValInline.InlinedDefinition` and reused the now-"free-looking" `0x00` bit pattern for it — but with the *opposite* semantics, `ShouldInline = false`. The reader was changed so `0x00` decoded to `InlinedDefinition`. That silently reinterpreted every `0x00` inline value already sitting in shipped DLLs: a required-inline definition from an old library now imported as non-inline, so the consuming compiler emitted a direct call to the SRTP dynamic-invocation stub instead of inlining the resolved witness. + +The violated assumption is the "tag values are forever" rule: a bit pattern that already has a meaning in shipped metadata cannot be given a new, incompatible meaning. + +## Why It Escaped + +PR #19548 *did* add write-side normalization so a current compiler serializes `InlinedDefinition` as `Always` (`0x10`), keeping fresh round-trips correct. That protection is exactly what hid the bug: + +- Any in-repo test compiles the producer library **with the new compiler**, which never writes `0x00` for an inline value. So no test that builds its own fixtures could reproduce it — the poisoned byte only exists in binaries produced by an F# 5.0-or-earlier compiler. +- The `CompilerCompat` cross-version suite exercises recent SDKs (9 ↔ current), not pre-2021 F# 5 binaries, so the format generation that still emits `0x00` inline bits was outside its matrix. + +The gap was read-side: the new meaning was applied to old bytes, and nothing in CI reads bytes written by a 2021-era compiler. + +## Fix + +PR #20260 adds `ValFlags.OfPickledBits`, used by `u_ValData` when importing metadata. Because the write side always normalizes `InlinedDefinition` to `Always`, a serialized `0x00` inline field can only originate from a legacy compiler, where it meant required inline. `OfPickledBits` therefore maps legacy `0x00` back to `Always` on import. The serialized byte layout is unchanged; only interpretation of the legacy pattern is restored. + +## Timeline + +| Date | Event | +|---|---| +| ≤ 2021 | F# 5.0 and earlier encode required-inline values (`PseudoVal`) as inline bits `0x00`, `ShouldInline = true`. | +| 2021-07-19 | PR #6811 removes `PseudoVal`; `0x00` still decodes to a `ShouldInline = true` value. Old libraries keep working. | +| 2026-04-16 | Commit `761c8635b8` adds write-side normalization for the upcoming `InlinedDefinition` (`0x00` → `0x10` on pickle). | +| 2026-07-02 | PR #19548 merges: `InlinedDefinition` reuses `0x00` with `ShouldInline = false`; reader decodes `0x00` → `InlinedDefinition`. Latent regression for legacy binaries. | +| ~2026-08 | Ships in .NET SDK 10.0.400. | +| 2026-08-13 | Issue #20253 filed: Aether 8.3.1 SRTP call throws `NotSupportedException` under 10.0.400. | +| — | PR #20260 adds read-side normalization (`OfPickledBits`), restoring the invariant. | + +## Prevention + +The generalized rule — flag bit patterns baked into pickled metadata are permanent, and a pattern that already has a meaning in shipped DLLs must never be reinterpreted — is encoded in [`.github/instructions/TypedTreePickle.instructions.md`](../../.github/instructions/TypedTreePickle.instructions.md), whose `applyTo` covers the flag-encoding types in `src/Compiler/TypedTree/TypedTree.{fs,fsi}` and the pickle path. When adding a case to a serialized flag enum, either allocate an unused bit pattern or add read-side normalization that maps legacy patterns to their original semantics — write-side normalization alone only protects future binaries, never the ones already in the wild. From eecc66912520a8fe4b3d40d2c7ba2e21c3890246 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Mon, 17 Aug 2026 10:26:38 +0200 Subject: [PATCH 15/15] Bump FSharp.Core to 10.1.401 (10.1.400 published to nuget.org) The Check_Published_Package_Versions FSCore leg restores FSharp.Core at the version in eng/Versions.props and fails when the restore succeeds, meaning that version is already on nuget.org. FSharp.Core 10.1.400 has now been published, so bump FSBuildVersion 400 -> 401 to the next free servicing version. FCS moves to 43.13.401 (also unpublished). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- eng/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index f2902fb245e..b0b38e6b0f6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -19,7 +19,7 @@ 10 0 - 400 + 401 0