From 0ab20352a8946f600d2267a93ca79e374f136318 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 16:07:09 +0200 Subject: [PATCH 01/14] 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/14] 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 ba720eb4c387b7646a822b1c41a7762267db39b9 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 17:01:54 +0200 Subject: [PATCH 03/14] 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 zcmeHNX>26dajy5e=j3pP93Fd-+@Xdm?ZF+MdrB+q!KIeTUJ@xeu^lXj!zMM-aGKlG zvn#G5g@P0ZHj>yzj5tDMLrx+fL5yUT19lR}*dO{4AQ56DP+&tzAUlBmBu11A$N^~c zRdvry&+ym{0yse6M|SnAdR6u6)w}Awo=YG992rDp;<O3w zPjpckG_1e*O0Bl{DLL4tqC^KlVZ)HVIRlWvGmnR@%iyK!#WgmfH#|BZ+I2ca)FJ4S|>_uAl?TdLxdL8Qxy1xI!2Upt><0fZyx|anY6W~evs#L zPQ@vpSZtd|hW#;&Cy-9VAvwn?* zwecou)HXUJ4CP&;1|32j(A(*i#6>#gAq^EGC8+2b7cP@66dT9&et0F$jbgRr*^n@* zT-mVR0WP+p_ak*s>P&(MUD9dn-wCmJxUx%{)Ka$FE8F9hU6wjStf#K5Yt*Ffn*CB) zJUkrLVH?9dj7m@H{r!;E#)|`@@2oPtDS8 zKZFyqXG``1a85wg$9DWmEQT>h|EVSjI?*@u03Mz{a3`Vn34;N(6J_wQzab}l{N|^< z1!p<*jo*@5i^8-_^Qht%1pY1-i!l9(O=l6Ez0=n z6WNmFGgP4WXpml^q#C3O8GtVU9+3P2KhLE(%==@M!-{bUa1Xr-n5AC?JVd_=I6=1o z9~SyKfjY5kIyif}Ccl5e0fb(m59c##_dP7RzkkUJnza#ng zB~OaAWdxoUI4^Kr;2Q!-3y;9lKIC?-OMYE*{x$V482t*P;kS(c0{krPP`pudF%w!Eg-yJ){>&KWKD|HNVJm<)rB~N5ltUr zA?N%A9b9C7PanChx4y7MII;x9V6^c{iU6GWiTwR^8%C@mAg~+U1Qrk6f}B zt0n7Enq8~7)(Tx&c-AVoRJPoTn=dcsokeQWd9k?QU6bSm>>5Mub^4{=;1MXWY*0)Zguq>0`Q?=+a9GRbQ{K8Gvk>`#adY? zp>LM#ZKZOT&(EQDaqIBYmQ#V7PTd&KjZ#g;85a@6Le;e@ek*3muH`J{3(~#5NS%pt z1=n_Ju79#xMo(Kgp9_Hq&E=hC%bms{#JXua&ou(??re2+)plHK z@%-}z3nSky(@gn>{hUQb?`4Bnpd0y8)go?dvE((#S+1^FW%ry7FIF0L)1CdJhiqd!T@wFx>hY0uUD;WJdbk) z8Acc>g7TZ~zlvDaU7D7Tn?FKNm5VN&f8KTSbN1>*>xNajvdVVbPQ_#OIOl9H2yPai zq-)p2$f;swwUl3@IvaZ-?^ts-*$dCsRtL+W0_gO(2rr#>8H*be zC+l6{x1r(V3Yku?$5R!<(YJFYX9^(e2N6UVS&y{>k?! z`e!e6-F@fq-#iiSNyk(=fmbJ9(c6z*`>D=%<0q3n>3-d%1R_QVXABjMMmto~u82TA zqg3*!qFq}qUW(?D-Ssubcg|%jIn=2r5nN$$DpwSF(kfZ`isd0=I91^fd!nL{AZQBP zG@u4_IHS5$7sWaZJ)j4090|dg{+f*nt>y=jKggX=M4?+yV?RZW3@MXKr7Vv!FPTwF zCyX#22qVOVUox77VHqE@yaefJM^D-cu_ft5GF?n2lc^4+2B;@}-5ek$#Ny&S6i0kE zi1l^0h@qwoC1FK0oq#W+eP)C^Bcr2z>8g;|*o-w$bYFUHPfvRNWi1Xsdp$?^7`lSG z6r9r`zUV}wxunsB9g!A+T!;2ld31u^iEZ>}0%gelv}!c~b$3hM6Wc4^R2XfAE8zm& zw_8!q3(X6{{sY71>o213YbVqc#%r)~((W!eAd&H!jWH|U=+>N}zeikVP+Kr?X$wXO zO#?C3**4uoh*ZnZmF=75u(vTr{Jk{Z*h_bAwU-8~9Ix%G8@A%yR^lPq!`pp9Z|&TA zuEpx%PuOdm$>C2JTMzc6%_m{JvB~eO?Z>i&1~&PFHM{HaKyEA6fkwe*@xbVInl=-t zEeCxyy_*FRxvjNs#vbukm3=jx+r}TqwpPBaoVQ%necBqmY~#~b*4~<(&4JGTtqp5t ztlQ8U5p82bE9;FB4l61nuDz14EF;9Ou8!miHsT}O9jRCqj#UriOyF80laq^M3*-4C zhewxAOdg&*dNhA{;l%jl;qhZ*$H$J0P8=~on%#TMoMB{BY;vY6*^%%iGxMCnqFc6uv1AZ?>PU z7E6ohTbFN7mwqtDVyDscpr(g39Wdy?A(c>Vx@~F~nc6Tz%4|!t<7QBIrWtfI*uh{o zgMJ2k84L>Jek~eUW>^S^N$VV*5%5e*GB_giXtn`HfcH_n545Q`l%zU3 z(_P)4!7VtV6y6azeBl^I^IfQ_hjxVP4^ABSL>2BFHZ6^DoRma>B|xQET@rzw2cTjL zIli*HmsYkcDdyp3W(z%hZGA62y{v};M`c^+!3TqT>eOM9OToAQfk;YoNIz!F6A0g5k8-QZG$2xYgVLH5_*(5 zI;MB&486+cu;y{o&uyw%Z_td&wFe00=^75vtt&5#*3_&;QZ)_)E5Lbab1 zMzx6hf^dvFlucw3S!VEE65%RSds6)ZLOL>(O&v)c^JF=cib{GT#YA-}(|MEeUD?sZg{`xcJk zNk{n7hB+-uEAKtLwDQB-mjC4&I|BZ~_)*9?Gj4WxYZ8k6gqedsaCngvM^mjzFnSGy zS#2~1XLa!+7fil_Y0!8*;DNYN0oI=>h`kbm0-am!Np`A^*al`P#!jVez>tRzGDmE*a!@fe%}J=rUdWsEPa=lFg4k!L!ZA zV~xp2AUVH;pmJhdPCvMLDY|Ez<)H2R>$e=ik36}J_q}_zx$h0Of5<;c2hMA`4W};u zfYo%iQu+U@b8+jb*gx3SE~-=^c))8uKw*?(IY7Xs4P}NX91$4R22K_XQi691ObhIS zz+OC=C}hk~I1+tz?l`YZfmGXuWW(cFf4iivXX%qG-o+--v8JN_-G~VZx9_Gr07H*LS2dfp$XfxP!iWh5W<#hnHA}0)Z*JOFQ z#C+prmX}_iob+wPiWJgOu3T?o5Tab1Y8nQ`NGG(OgbbU4J)CQ0!y!%@=6Ylf7_rbH z!5O|qcq6ed5W|U$5w03>GHTeQ>JWY+r6P^0YrY5=?)d_W*%vr_CE$7dRswFUeJOxy zS+IK1=X?gnckxZmpu4zab2)CC^bGGvb+i43><+lrL_Fz{X%BBR)Ysn#m()OibGQV9 zV>)2*V zBFHrPY>C9K;R zM%qwJKR44T{cX-?8h9}Gl>lFZehqisQ>D_Se6dU`l>#oly&pn=h+jPh;5t8S z{D`~Jdziv`qQ?Y!XHSpf6YzQ;1OtPhKXYhrw0{#_HOZa}7fsGclRiKBzR}zikOCx`6^d9~xEz8yFY31I?KPmiI zq0`3K%PPKxy7#Q(BBCb+zi_LTrDgnev{tA0jn#r}7h&r%<)L#;T3ixd3)U`zzXT3K z$kiUOcM1{}@U!MsSW;`{*1A~7Bch|Z4VO{x3bb+B$__5`WY3}XE2uFKpRS?T?@x9L z@(SL{!`@o!8$C-jg!P(e4yCNOf)+QvG;`hf1v4}JFx`&_PJ6!@zazizy&r)e^8A1D 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 754a7d4b3a439319acd044ee8c88e5c28ad04015 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 17:04:52 +0200 Subject: [PATCH 04/14] 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 ddab2574c734b8785175ba42da5d3555b9236fe5 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 17:29:52 +0200 Subject: [PATCH 05/14] 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 6fc111d3e71159078addf62f938201e48f4de603 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 17:41:36 +0200 Subject: [PATCH 06/14] 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 236a4a9b798..436e8b44b17 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 98c4ab0e840..f2226788d47 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 5b64b10f600..81891a408f2 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 5cff87d95393429f6f2d1fd40df2ad6d8fdc8535 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 19:48:05 +0200 Subject: [PATCH 07/14] 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 e39f6d8e10a2b934b87f2e5a0c6da8e935ce2d81 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 20:01:29 +0200 Subject: [PATCH 08/14] 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 3a7c86cb18e6515f44179acb66bbadad7b668096 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 21:30:47 +0200 Subject: [PATCH 09/14] 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 436e8b44b17..44e17e3b667 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 f2226788d47..dede50853d2 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 2e4f14b298ce6b4237586d77921c780a20eafdb7 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 21:59:02 +0200 Subject: [PATCH 10/14] 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 28ba693ed8d52f96183f2106ffd2858a9b9def91 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 13 Aug 2026 22:14:42 +0200 Subject: [PATCH 11/14] docs: note legacy inline metadata fix Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 7dd68964cb3..9b5ad43cfe1 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -44,6 +44,7 @@ * Extend the `=` adjacent to an interpolated string fix to the verbatim (`=$@"…"`, `=@$"…"`) and extended multi-dollar (`=$$"""…"""`) interpolated-string forms. ([Issue #16696](https://github.com/dotnet/fsharp/issues/16696), [PR #19984](https://github.com/dotnet/fsharp/pull/19984)) * Preserve type abbreviations (`string`, user-defined aliases) in the refined type of bindings introduced after a `| null` pattern in a `match` expression. ([Issue #19646](https://github.com/dotnet/fsharp/issues/19646), [PR #19745](https://github.com/dotnet/fsharp/pull/19745)) * Fix attributes on return type of unparenthesized tuple methods being silently dropped from IL. ([Issue #462](https://github.com/dotnet/fsharp/issues/462), [PR #19714](https://github.com/dotnet/fsharp/pull/19714)) +* Fix cross-assembly calls to inline SRTP functions from libraries compiled with F# 4.7 and earlier. ([Issue #20253](https://github.com/dotnet/fsharp/issues/20253), [PR #20260](https://github.com/dotnet/fsharp/pull/20260)) * Fix enum values losing their type when used in a custom attribute argument of type `obj` (they were stored as the underlying integer instead of the enum). ([Issue #995](https://github.com/dotnet/fsharp/issues/995), [PR #19975](https://github.com/dotnet/fsharp/pull/19975)) * Fix false-positive nullness warning (FS3261) when pattern matching narrows nullness inside seq/list/array comprehensions. ([Issue #19644](https://github.com/dotnet/fsharp/issues/19644), [PR #19743](https://github.com/dotnet/fsharp/pull/19743)) * Fix internal error FS0073 "Undefined or unsolved type variable" in IlxGen when nested inline SRTP functions with multiple overloads leave unsolved typars in the non-witness codegen path. ([Issue #19709](https://github.com/dotnet/fsharp/issues/19709), [PR #19710](https://github.com/dotnet/fsharp/pull/19710)) From bb6ead4a5b1fb88da14f3f8feae2b254b83d9e99 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 14 Aug 2026 12:36:49 +0200 Subject: [PATCH 12/14] 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 zcmeHNeQX@pai8~g?~}(n^7x@Ik~;b*nZ#7#b@%R)P`fPIob78E?X?x<S-Af$-Uf?5g#=O$EBrffZ51~NMgkix@lZK@yS zrHoT@awrnpmXKk8jN$R688{^4SOpu5V(S(PS=R&}Uz#C0<3lC8$1{pwngY~u57Dgz zk@6UCA0ax>+bp#+{!;H)xcf~Qd0@27fs&0gPVb??ZY5jWQ}0&PdN z@g{22b{N${dDnzNqo@OVJKd6~NGDvRu0o^)6+M%}WwM2080JA#dO{EPLs}az4v3P6^$VO~{~u&Ry*yPd=S;2q%)(EdQC{!!Sh_ZynaE5{ zOdWX;2*yVNa41NWeu?M{c;CbfOD~k2d~vPB91{@Q!aM!s0^LF$W>NalC+BDJz76~X zI3azeU@rsb1XO)u&#y-!7<2SrYKou}eM9%)L6>0;6QTDBg8{V@rSP!7At!wN=BK?0 zXF2qZ*OF?Bg0x0UsN$Ceeglg|kiIARzZUpoP@*)T!YWGt3>c&b^Z^~DC4C8SQ{Yzw z{(-=E1pb@AHiLPFj3sDTG+4un0$&FV(q9_CXP{~3PXPM`9tVt)V>0K<0$&sOb-*CK zZ?bJF!1iF%^g~S_2*rh5TBUFgYI-KX{BHh5$@?|DArx10HE&?#7?=?T#*2a3W6(Z2 zO+9*o<^j*s;{u-+m=%~8cvax0z?TJnM&J;gFy5jsBL4$=9q=FNw*l|a*9HC|U|2;Y z&!vae7~pyO5q;HsLQRY8Num5F-5dBFHA{~N{z_c{7#9H#(5rxH`W3)Y`VGJ-x&`=< z(9a6|Y0%HobM!Cp4{M$mZSzvsl2Dd}lB2(;H*^l1-_%=JLnhQsDZMGBw~1 zpTgtT%NDqr47zS`nL(QdQw(}RR;RE>4}%Ue>~#5}9^UWa{T_bM!w&*KOv|nf7p%2x zZgaj^$QP~5N})h=3(sVojfMPLF;K~)u}%ZMPBD`l(XwPL$PP*3z}9;(bHDO<3b=pi&R!y33sx(+MbRa@yinQLu${8C za_;$@g^_O;X});Pe$Jx2`?5jI(Y0)$ViC7BUvL}btX0;nV)?8MFP7?cWoa1;u8;9z zc4Gsst#d5c6(?uGJ0%(IbA>Ddz{TuZbGNuUEN8)TuH|#+;40x!t7yTi){0N3OvcP? zKD$=5OE9=Zv(|EDZOw8n*~KSImcygB&R;D&B=bfr?vU#(bIcphhR zGK?@%1m!o|{}^J~^6H#)+|mQ|WHDc+bI+HZ?4rGK!MbJ@E^n~iwo`IhUCu?@4T2lR zr|HTSF>*Fv+9+fDh4qeC= z*DBdH3x=-e%lT{YDcdgo%;#ju@Uy#X2rX7NuwYFJ>pYgc$%Z9{u3*Ykmk?e$?IIR8 zCQjG7z-vR@$0azC)@ErFdq|enDUTtO0xZ%svXReH8JG<`1-L43gRbBl%q8dODpimx zV6S8;hDGp|@h+ic1AjI-z%9fB)P4y2_XKwCSy`?=Sy z9)9{>3jg^FU3cET|8E};_9P=J8N;g+ukfu$uKYsh527dHJ;{FEr34~I2&N1b4u?Bb z!>))xJ*8CqsG@y4E?$ZjbaVl37dcrDL*^=cVM{ugbANE8= z0YPxwVVefjfDWcqm+GQOhoJ}b0FEO87}H<1QK8lRAo2&f^YJir3u+8g*hrBwxm3!s zD07n;rew?r;(;(iO!y_GSs0e_G0RPm4tMkp?V}n>LnEgp)D>di|xHD2Z+?T8fd6Ui91V#5HHxKkAw_eub0JPV2 zgomLks7t{)9pZ~lG@485UDy$763BFDSCvaA*d5zNf67;e?59<;0jRrM>YmzN@wUQf zD_jW|=)V1mdR}N;5QYzr6|cUCzOSB86Bw`l#!0)o;DA`lZ8pZNc%xf$hTcM4W>A|y zaB1^L2n_=<(%ClGKnPWe&lT?xNRqu9zL}q8L+p&kdRpn4s=dSU`v7MFgD(5a&HJ>&|FT41(nYFiSXJeo< zyt84Aj5QmYBcfewXlCsm$NlQKf3?o6mQ+?P2QOH~($ZuIyKHHFX)??K6mNJn!fSu(c?EZ zSC3zxzFJtG&P^b#6~`CHciekw!?d;;mtc0lEoPdIEbYU*rb|L0Jbj19pq@Wvwsz(A6h6W@IJt>}t z-^RV2aoo3v;c(E{Db=CELlaGHKU2vUR^a$DUp$(u3QMS_do|ss>99eEM^#L<>9*N@ zWNO0(SnPkw-U=M@+4Eh-yWH2apAg;9#%L^ zZ(ADS03eRYL4ZoIx;P>j7eK`ha(owcH?3@0T+G9j!w!1*KK5>UdRY$x4mWntgPZWX z>ezpI`;*292sg}H-0xG2A?9-n_gJLyUN@AQOs(sR#8&{Ovhn5(G+ zR-&n&!7$<$W4uVh#}-L^FRpQ~gjZTcmGqKZ;zCdS72H?zaKuYelQ>5AP}+zZG2Edc zsY7WaZgjZFy_7ZrMg&8hO9&H?Ol9ytF05*)1M1mOOg0W;+=pv)CxOcc6$a*)9!dw> zg6*z)<4`)72zI*2eUuIcf>C$oNL^a1h%ya>|Fv0H)=(eY;eXuP#9;;s4pTZ3io`Jq z8j|Z8)UtwmSNw}}SdHH~s*OfSrK6E(On{-3p|mo#zJ|HZ)0UTj5e~zZQe&RsTod$i zDc6XK@Ld3GEsCJ5QIS@0=uzgx5xq~R=v6j{HBXveZd=W|MKdbbD$uMQ(*>)Nn)J*( zifJh7Yf3%;dIlTES_}>~sXWH2;DLIJq0kdITg9=`G_>~^?wYxINBIje=W3St9T#}0 z*$!zQKDIO{dq|Y|o98KI-%D>aHZOPzYGVgt{Q?3yFqDo>$Bw!qF?c8)J03gXB3L~Y z{MMt*fEhISKVSy7eiQaWwU-n|wTOFyaEuyF$5OF0Gk7iuag~V!iGBeA9U4j}9!MN> zYY87pCmu}jWj2ymWv@0vX4v5WkQv&hN)h)2-6|8Qgulvgi}TQuX2)?S`i5`KVg6o- zv?@aT8+$;jJ6DSCK)uyV3PE8DsswmdpAr(CLPfgWKaS(ukK=YTZt#D*+3puj_{U?%_Tw>Ob{PDh zFcSf-pPOaLxcZR(0U*+`S2i-e*h|{Cgt3K8-;xNw6o^7eYkdd_kX;|wBBPL z*ySG6-~J)*gy=izWLnNE-qEDtRHCxmpZA(id)|SidOf0Y{)0>7*#koea~08)Hk28l zU`SwC8wmRuBn0mim=xFrfrEHbVaS+)U?}|R;&EO!e5txe$Zo`e_-;vkhsT*OHApM{ z)m@6)c&UQryijmRh=l{mPr?Ek&?f9tJdBn5<0GkoX}k$5J;s#_E!ZRt4pu9g(Ppse z1h1sf%99keA}0)ZS7muE!))L+kk>_z9QSO*auLvBu3T?p5JFHKIqC*QNXN9Eh76m6 z4VG(V!y!%@=DK8#=CBGO!5N-Kcq6ed5MYUoA+8!hENa-M>HvP2qeAtntG@6V?)n0X z*%vq%#o&2-p@$o*_tQ`C;`NZ+lU4g`!>(#djl|m@ zTSqjAzA-`p!|b~Uok7(to=lSuen{*}K3gEr0_%Xw1s6~-UC*;ifl$UDb5mMe5MB$`u7JM)4*aedzyGS+I}3>t_*wG?EOEceGT%=/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 6ad68f18a3948891dc214dab14bbafca0be38af2 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 14 Aug 2026 12:37:17 +0200 Subject: [PATCH 13/14] 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 df55ec38ae5793fca022759715498cb7b8851aea Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 14 Aug 2026 15:38:03 +0200 Subject: [PATCH 14/14] 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.