From 7ad68b7a6841c0bbfce73a85a0435c03afd1d346 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Wed, 20 May 2026 08:45:53 +0200 Subject: [PATCH 01/16] Fix EI_ilzero optimization: treat Unchecked.defaultof as effect-free for concrete types Unchecked.defaultof<'T> (compiled as EI_ilzero) was unconditionally treated as having an effect, preventing the optimizer from eliminating unused bindings like `let _ = Unchecked.defaultof`. Fix: In ExprHasEffect and OptimizeExprOpFallback, EI_ilzero is now considered effect-free when all type args are concrete (not type parameters). When type variables are present (e.g. SRTP ^T), it is conservatively treated as having an effect to prevent orphaned type variables during IL generation (FS0073). Fixes #17775 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Optimize/Optimizer.fs | 13 ++++++++++- .../CodeGenRegressions_Observations.fs | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index 3cbb574598c..b63c30533a1 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1629,6 +1629,10 @@ let rec ExprHasEffect g expr = | Expr.Const _ -> false // type applications do not have effects, with the exception of type functions | Expr.App (f0, _, _, [], _) -> IsTyFuncValRefExpr f0 || ExprHasEffect g f0 + // EI_ilzero (Unchecked.defaultof<'T>) is effect-free when all type args are concrete (not type parameters). + // When type args contain type variables (e.g. SRTP), we conservatively treat it as having an effect + // to avoid issues with orphaned type variables during IL generation. + | Expr.Op (TOp.ILAsm ([ EI_ilzero _ ], _), tyargs, [], _) -> tyargs |> List.exists (isTyparTy g) | Expr.Op (op, _, args, m) -> ExprsHaveEffect g args || OpHasEffect g m op | Expr.LetRec (binds, body, _, _) -> BindingsHaveEffect g binds || ExprHasEffect g body | Expr.Let (bind, body, _, _) -> BindingHasEffect g bind || ExprHasEffect g body @@ -2641,7 +2645,14 @@ and OptimizeExprOpFallback cenv env (op, tyargs, argsR, m) arginfos value_ = let argsFSize = AddFunctionSizes arginfos let argEffects = OrEffects arginfos let argValues = List.map (fun x -> x.Info) arginfos - let effect = OpHasEffect g m op + + let effect = + match op with + // EI_ilzero (Unchecked.defaultof<'T>) is effect-free when all type args are concrete. + // When type args contain type variables (e.g. SRTP), we conservatively treat it as having an effect + // to avoid issues with orphaned type variables during IL generation (FS0073). + | TOp.ILAsm ([ EI_ilzero _ ], _) -> tyargs |> List.exists (isTyparTy g) + | _ -> OpHasEffect g m op let cost, value_ = match op with | TOp.UnionCase c -> 2, MakeValueInfoForUnionCase c (Array.ofList argValues) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs index 4eb3dc57b98..b702730d81b 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs @@ -218,3 +218,26 @@ let empty<'T> = Seq.empty<'T> .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 09 00 00 00 00 00 ) """ ] + + // https://github.com/dotnet/fsharp/issues/17775 + [] + let ``Unchecked_defaultof_unused_bindings_eliminated_when_optimized`` () = + FSharp """ +module Test + +open System + +let f (n: float32) = + Console.WriteLine n + let _ = Unchecked.defaultof + let _ = Unchecked.defaultof + let _ = Unchecked.defaultof + let n' = n * 2.f + Console.WriteLine n' +""" + |> asLibrary + |> withOptimize + |> compile + |> shouldSucceed + |> verifyILNotPresent [ "initobj" ] + |> ignore From 8e663384d861b365eb747612c281d5fdfafc167f Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Wed, 20 May 2026 10:19:23 +0200 Subject: [PATCH 02/16] Use freeInTypes to detect nested type variables in EI_ilzero check isTyparTy only detects direct type parameters (e.g. ^T) but not type variables nested inside constructed types (e.g. SomeType<^T>). Replace with freeInTypes CollectTyparsNoCaching which recursively checks for any free type parameters in the type structure. Also add release notes entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Optimize/Optimizer.fs | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) 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 4a6110cf514..a592b2dd52f 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,5 +1,6 @@ ### Fixed +* Optimizer: treat `Unchecked.defaultof<'T>` (`EI_ilzero`) as effect-free for concrete types, enabling dead binding elimination. ([Issue #17775](https://github.com/dotnet/fsharp/issues/17775), [PR #19758](https://github.com/dotnet/fsharp/pull/19758)) * 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 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)) * Fix NRE when calling virtual Object methods on value types through inline SRTP functions. ([Issue #8098](https://github.com/dotnet/fsharp/issues/8098), [PR #19511](https://github.com/dotnet/fsharp/pull/19511)) diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index b63c30533a1..7c1fec3ef72 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1629,10 +1629,11 @@ let rec ExprHasEffect g expr = | Expr.Const _ -> false // type applications do not have effects, with the exception of type functions | Expr.App (f0, _, _, [], _) -> IsTyFuncValRefExpr f0 || ExprHasEffect g f0 - // EI_ilzero (Unchecked.defaultof<'T>) is effect-free when all type args are concrete (not type parameters). - // When type args contain type variables (e.g. SRTP), we conservatively treat it as having an effect - // to avoid issues with orphaned type variables during IL generation. - | Expr.Op (TOp.ILAsm ([ EI_ilzero _ ], _), tyargs, [], _) -> tyargs |> List.exists (isTyparTy g) + // EI_ilzero (Unchecked.defaultof<'T>) is effect-free when all type args are fully concrete. + // When type args contain type variables anywhere (e.g. SRTP ^T, or SomeType<^T>), we conservatively + // treat it as having an effect to avoid orphaned type variables during IL generation (FS0073). + | Expr.Op (TOp.ILAsm ([ EI_ilzero _ ], _), tyargs, [], _) -> + not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars) | Expr.Op (op, _, args, m) -> ExprsHaveEffect g args || OpHasEffect g m op | Expr.LetRec (binds, body, _, _) -> BindingsHaveEffect g binds || ExprHasEffect g body | Expr.Let (bind, body, _, _) -> BindingHasEffect g bind || ExprHasEffect g body @@ -2648,10 +2649,11 @@ and OptimizeExprOpFallback cenv env (op, tyargs, argsR, m) arginfos value_ = let effect = match op with - // EI_ilzero (Unchecked.defaultof<'T>) is effect-free when all type args are concrete. - // When type args contain type variables (e.g. SRTP), we conservatively treat it as having an effect - // to avoid issues with orphaned type variables during IL generation (FS0073). - | TOp.ILAsm ([ EI_ilzero _ ], _) -> tyargs |> List.exists (isTyparTy g) + // EI_ilzero (Unchecked.defaultof<'T>) is effect-free when all type args are fully concrete. + // When type args contain type variables anywhere (e.g. SRTP ^T, or SomeType<^T>), we conservatively + // treat it as having an effect to avoid orphaned type variables during IL generation (FS0073). + | TOp.ILAsm ([ EI_ilzero _ ], _) -> + not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars) | _ -> OpHasEffect g m op let cost, value_ = match op with From ccefa82f8251c3a3401c077ff8488895efc9492d Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Wed, 20 May 2026 10:42:46 +0200 Subject: [PATCH 03/16] Use freeInTypes to detect nested type variables in EI_ilzero check Broaden the type-variable check to cover ALL effect-free Expr.Op expressions, not just EI_ilzero. When any operation would be effect-free but its F# type args contain free type parameters, conservatively treat it as having an effect. This prevents dead binding/sequential elimination from orphaning type variables that are only referenced through the eliminated expression. Also add EI_ilzero to the instruction-level no-effect list (its safety is ensured by the tyargs check above it in the pipeline). Also add release notes entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Optimize/Optimizer.fs | 34 ++++++++++++------- .../CodeGenRegressions_Observations.fs | 21 ++++++++++++ 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index 7c1fec3ef72..e52880f880c 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1615,7 +1615,8 @@ let IlAssemblyCodeInstrHasEffect i = | ( AI_nop | AI_ldc _ | AI_add | AI_sub | AI_mul | AI_xor | AI_and | AI_or | AI_ceq | AI_cgt | AI_cgt_un | AI_clt | AI_clt_un | AI_conv _ | AI_shl | AI_shr | AI_shr_un | AI_neg | AI_not | AI_ldnull ) - | I_ldstr _ | I_ldtoken _ -> false + | I_ldstr _ | I_ldtoken _ + | EI_ilzero _ -> false | _ -> true let IlAssemblyCodeHasEffect instrs = List.exists IlAssemblyCodeInstrHasEffect instrs @@ -1629,12 +1630,16 @@ let rec ExprHasEffect g expr = | Expr.Const _ -> false // type applications do not have effects, with the exception of type functions | Expr.App (f0, _, _, [], _) -> IsTyFuncValRefExpr f0 || ExprHasEffect g f0 - // EI_ilzero (Unchecked.defaultof<'T>) is effect-free when all type args are fully concrete. - // When type args contain type variables anywhere (e.g. SRTP ^T, or SomeType<^T>), we conservatively - // treat it as having an effect to avoid orphaned type variables during IL generation (FS0073). - | Expr.Op (TOp.ILAsm ([ EI_ilzero _ ], _), tyargs, [], _) -> - not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars) - | Expr.Op (op, _, args, m) -> ExprsHaveEffect g args || OpHasEffect g m op + // An Expr.Op is effect-free when its op is effect-free, its args are effect-free, AND its type args + // don't contain free type parameters. Type variables in tyargs indicate the expression may serve as a + // witness/dummy for SRTP resolution; eliminating it can orphan those type vars causing FS0073 in IlxGen. + | Expr.Op (op, tyargs, args, m) -> + if ExprsHaveEffect g args || OpHasEffect g m op then + true + elif List.isEmpty tyargs then + false + else + not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars) | Expr.LetRec (binds, body, _, _) -> BindingsHaveEffect g binds || ExprHasEffect g body | Expr.Let (bind, body, _, _) -> BindingHasEffect g bind || ExprHasEffect g body // REVIEW: could add Expr.Obj on an interface type - these are similar to records of lambda expressions @@ -2648,13 +2653,16 @@ and OptimizeExprOpFallback cenv env (op, tyargs, argsR, m) arginfos value_ = let argValues = List.map (fun x -> x.Info) arginfos let effect = - match op with - // EI_ilzero (Unchecked.defaultof<'T>) is effect-free when all type args are fully concrete. - // When type args contain type variables anywhere (e.g. SRTP ^T, or SomeType<^T>), we conservatively - // treat it as having an effect to avoid orphaned type variables during IL generation (FS0073). - | TOp.ILAsm ([ EI_ilzero _ ], _) -> + let opEffect = OpHasEffect g m op + + // If an operation would be effect-free, but its type args contain free type parameters, + // conservatively treat it as having an effect. This prevents dead binding/sequential + // elimination from orphaning type variables that are only referenced through the eliminated + // expression, which would cause FS0073 during IL generation (common with SRTP patterns). + if not opEffect && not argEffects && not (List.isEmpty tyargs) then not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars) - | _ -> OpHasEffect g m op + else + opEffect let cost, value_ = match op with | TOp.UnionCase c -> 2, MakeValueInfoForUnionCase c (Array.ofList argValues) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs index b702730d81b..18a797344e3 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs @@ -241,3 +241,24 @@ let f (n: float32) = |> shouldSucceed |> verifyILNotPresent [ "initobj" ] |> ignore + + // https://github.com/dotnet/fsharp/issues/17775 + // Regression: Unchecked.defaultof with type variables (including nested) must not be eliminated, + // otherwise orphaned type variables cause FS0073 during IL generation. + [] + let ``Unchecked_defaultof_with_type_variables_compiles_with_optimization`` () = + FSharp """ +module Test + +type Wrapper<'T> = { Value: 'T } + +let inline f< ^T when ^T : (static member op_Explicit: ^T -> int)> (x: ^T) = + let _ = Unchecked.defaultof< ^T > + let _ = Unchecked.defaultof> + int x +""" + |> asLibrary + |> withOptimize + |> compile + |> shouldSucceed + |> ignore From 79d9895ac028af9741d5d54ffb302e390613a7c0 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 29 May 2026 15:24:32 +0200 Subject: [PATCH 04/16] Refactor EI_ilzero effect check: narrow to ILAsm, add non-regression test The previous fix conservatively marked every Expr.Op whose tyargs contained free typars as having an effect. That over-broad check regresses dead-binding elimination for many unrelated generic constructions (e.g. let _ = Some x). Push the check down to where it actually matters: TOp.ILAsm with EI_ilzero. Only EI_ilzero embeds tyargs into IL and is the only newly-effect-free instruction introduced by this PR, so it is the only case that can orphan a free typar and trip FS0073. OpHasEffect now takes tyargs; ExprHasEffect / OptimizeExprOpFallback go back to a single line. Replaced bloated comments and List.isEmpty checks with pattern matching on []. Tests: split the bundled SRTP test into typar and nested-typar cases, and added Generic_Some_unused_binding_still_eliminated to lock in that the fix does not penalize other effect-free generic operations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Optimize/Optimizer.fs | 49 ++++++++----------- .../CodeGenRegressions_Observations.fs | 47 ++++++++++++++++-- 2 files changed, 62 insertions(+), 34 deletions(-) diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index e52880f880c..19c3291c8c7 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1612,14 +1612,24 @@ let SplitValuesByIsUsedOrHasEffect cenv fvs x = let IlAssemblyCodeInstrHasEffect i = match i with - | ( AI_nop | AI_ldc _ | AI_add | AI_sub | AI_mul | AI_xor | AI_and | AI_or - | AI_ceq | AI_cgt | AI_cgt_un | AI_clt | AI_clt_un | AI_conv _ | AI_shl - | AI_shr | AI_shr_un | AI_neg | AI_not | AI_ldnull ) + | AI_nop | AI_ldc _ | AI_add | AI_sub | AI_mul | AI_xor | AI_and | AI_or + | AI_ceq | AI_cgt | AI_cgt_un | AI_clt | AI_clt_un | AI_conv _ | AI_shl + | AI_shr | AI_shr_un | AI_neg | AI_not | AI_ldnull | I_ldstr _ | I_ldtoken _ | EI_ilzero _ -> false | _ -> true - -let IlAssemblyCodeHasEffect instrs = List.exists IlAssemblyCodeInstrHasEffect instrs + +let tyargsContainFreeTypars tyargs = + match tyargs with + | [] -> false + | _ -> not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars) + +let ILAsmHasEffect instrs tyargs = + instrs |> List.exists (function + // EI_ilzero<'T> embeds the type into IL; eliminating the expression would orphan + // free typars referenced only through tyargs and trip FS0073 in IlxGen. + | EI_ilzero _ -> tyargsContainFreeTypars tyargs + | i -> IlAssemblyCodeInstrHasEffect i) let rec ExprHasEffect g expr = match stripDebugPoints expr with @@ -1630,16 +1640,7 @@ let rec ExprHasEffect g expr = | Expr.Const _ -> false // type applications do not have effects, with the exception of type functions | Expr.App (f0, _, _, [], _) -> IsTyFuncValRefExpr f0 || ExprHasEffect g f0 - // An Expr.Op is effect-free when its op is effect-free, its args are effect-free, AND its type args - // don't contain free type parameters. Type variables in tyargs indicate the expression may serve as a - // witness/dummy for SRTP resolution; eliminating it can orphan those type vars causing FS0073 in IlxGen. - | Expr.Op (op, tyargs, args, m) -> - if ExprsHaveEffect g args || OpHasEffect g m op then - true - elif List.isEmpty tyargs then - false - else - not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars) + | Expr.Op (op, tyargs, args, m) -> ExprsHaveEffect g args || OpHasEffect g m op tyargs | Expr.LetRec (binds, body, _, _) -> BindingsHaveEffect g binds || ExprHasEffect g body | Expr.Let (bind, body, _, _) -> BindingHasEffect g bind || ExprHasEffect g body // REVIEW: could add Expr.Obj on an interface type - these are similar to records of lambda expressions @@ -1651,7 +1652,7 @@ and BindingsHaveEffect g binds = List.exists (BindingHasEffect g) binds and BindingHasEffect g bind = bind.Expr |> ExprHasEffect g -and OpHasEffect g m op = +and OpHasEffect g m op tyargs = match op with | TOp.Tuple _ -> false | TOp.AnonRecd _ -> false @@ -1665,7 +1666,7 @@ and OpHasEffect g m op = | TOp.UnionCaseTagGet _ -> false | TOp.UnionCaseProof _ -> false | TOp.UnionCaseFieldGet (ucref, n) -> isUnionCaseFieldMutable g ucref n - | TOp.ILAsm (instrs, _) -> IlAssemblyCodeHasEffect instrs + | TOp.ILAsm (instrs, _) -> ILAsmHasEffect instrs tyargs | TOp.TupleFieldGet _ -> false | TOp.ExnFieldGet (ecref, n) -> isExnFieldMutable ecref n | TOp.RefAddrGet _ -> false @@ -2580,7 +2581,7 @@ and OptimizeExprOp cenv env (op, tyargs, args, m) = newExpr, { TotalSize = 1 FunctionSize = 1 - HasEffect = OpHasEffect g m newOp + HasEffect = OpHasEffect g m newOp tyargs MightMakeCriticalTailcall = false Info = ValueOfExpr newExpr } @@ -2652,17 +2653,7 @@ and OptimizeExprOpFallback cenv env (op, tyargs, argsR, m) arginfos value_ = let argEffects = OrEffects arginfos let argValues = List.map (fun x -> x.Info) arginfos - let effect = - let opEffect = OpHasEffect g m op - - // If an operation would be effect-free, but its type args contain free type parameters, - // conservatively treat it as having an effect. This prevents dead binding/sequential - // elimination from orphaning type variables that are only referenced through the eliminated - // expression, which would cause FS0073 during IL generation (common with SRTP patterns). - if not opEffect && not argEffects && not (List.isEmpty tyargs) then - not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars) - else - opEffect + let effect = OpHasEffect g m op tyargs let cost, value_ = match op with | TOp.UnionCase c -> 2, MakeValueInfoForUnionCase c (Array.ofList argValues) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs index 18a797344e3..14522390249 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs @@ -221,7 +221,7 @@ let empty<'T> = Seq.empty<'T> // https://github.com/dotnet/fsharp/issues/17775 [] - let ``Unchecked_defaultof_unused_bindings_eliminated_when_optimized`` () = + let ``Unchecked_defaultof_concrete_type_eliminated_when_unused`` () = FSharp """ module Test @@ -243,17 +243,32 @@ let f (n: float32) = |> ignore // https://github.com/dotnet/fsharp/issues/17775 - // Regression: Unchecked.defaultof with type variables (including nested) must not be eliminated, - // otherwise orphaned type variables cause FS0073 during IL generation. + // FS0073 regression: free type variable in EI_ilzero tyargs must not be eliminated. [] - let ``Unchecked_defaultof_with_type_variables_compiles_with_optimization`` () = + let ``Unchecked_defaultof_typar_kept_in_SRTP_context`` () = + FSharp """ +module Test + +let inline f< ^T when ^T : (static member op_Explicit: ^T -> int)> (x: ^T) = + let _ = Unchecked.defaultof< ^T > + int x +""" + |> asLibrary + |> withOptimize + |> compile + |> shouldSucceed + |> ignore + + // https://github.com/dotnet/fsharp/issues/17775 + // Nested free typar (Wrapper< ^T >) must also keep the binding alive. + [] + let ``Unchecked_defaultof_nested_typar_kept_in_SRTP_context`` () = FSharp """ module Test type Wrapper<'T> = { Value: 'T } let inline f< ^T when ^T : (static member op_Explicit: ^T -> int)> (x: ^T) = - let _ = Unchecked.defaultof< ^T > let _ = Unchecked.defaultof> int x """ @@ -262,3 +277,25 @@ let inline f< ^T when ^T : (static member op_Explicit: ^T -> int)> (x: ^T) = |> compile |> shouldSucceed |> ignore + + // Non-regression: the EI_ilzero fix must not over-broadly mark other effect-free + // generic operations as effectful. A generic Some over a free typar should still + // be eliminated as a dead binding under optimization. + [] + let ``Generic_Some_unused_binding_still_eliminated`` () = + FSharp """ +module Test + +open System + +let f<'T> (x: 'T) = + Console.WriteLine "before" + let _ = Some x + Console.WriteLine "after" +""" + |> asLibrary + |> withOptimize + |> compile + |> shouldSucceed + |> verifyILNotPresent [ "FSharpOption" ] + |> ignore From d68c69b6f270cec080845c9ee84ee202e2e2ad1c Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Mon, 1 Jun 2026 12:26:49 +0200 Subject: [PATCH 05/16] Strip dead-code FS0073 protection; restore release notes; fix issue refs Adversarial review of PR #19758 surfaced three blockers: 1. The tyargsContainFreeTypars protection guarding EI_ilzero against orphaning free typars was never exercised. With it neutered to always return false, 447 EmittedIL + 728 EmittedIL.RealInternalSignature tests pass, including the actual issue #18128 SRTP witness pattern (FSharpPlus-style 'nil<^b>' inside an inline _call helper). No FS0073 reproducer exists. Remove the protection. The fix is now one line: add EI_ilzero to IlAssemblyCodeInstrHasEffect's no-effect match. OpHasEffect signature is restored, ILAsmHasEffect helper is removed. 2. The branch was based on a stale main and silently reverted 14+ unrelated release-notes entries. Restore the file from origin/main and re-apply only our entry on top, with corrected wording that does not understate the fix (the original 'for concrete types' qualifier is no longer accurate and never was useful to users). 3. The PR referenced issue #17775, which is an automated VS insertion PR list, not an EI_ilzero bug. The real issue is #18128. Update test annotations and the release-notes entry accordingly. Tests reorganised to three distinct cases that each pin observable behaviour: - Issue_18128_Unchecked_defaultof_concrete_eliminated: verifyILNotPresent ["initobj"] - the fix's stated purpose - Issue_18128_SRTP_witness_pattern_compiles_and_optimizes: the actual FSharpPlus-style pattern from the issue body - Generic_Some_unused_binding_still_eliminated: non-regression that proves EI_ilzero is targeted, not a broader effect-analysis change Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/11.0.100.md | 19 ++++++- src/Compiler/Optimize/Optimizer.fs | 22 +++----- .../CodeGenRegressions_Observations.fs | 51 +++++++++---------- 3 files changed, 47 insertions(+), 45 deletions(-) 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 a592b2dd52f..e1f921c04dd 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,9 +1,15 @@ ### Fixed -* Optimizer: treat `Unchecked.defaultof<'T>` (`EI_ilzero`) as effect-free for concrete types, enabling dead binding elimination. ([Issue #17775](https://github.com/dotnet/fsharp/issues/17775), [PR #19758](https://github.com/dotnet/fsharp/pull/19758)) +* Optimizer: treat `Unchecked.defaultof<'T>` (`EI_ilzero`) as effect-free so unused bindings (e.g. SRTP witness arguments) are eliminated under optimization. ([Issue #18128](https://github.com/dotnet/fsharp/issues/18128), [PR #19758](https://github.com/dotnet/fsharp/pull/19758)) +* Reject non-function bindings for single-case and partial active pattern names with FS1209, matching the existing multi-case behavior. ([PR #19763](https://github.com/dotnet/fsharp/pull/19763)) +* Fix FS0421 "The address of the variable cannot be used at this point" incorrectly raised for the discard pattern `let _ = &expr` when `let x = &expr` compiles. ([Issue #18841](https://github.com/dotnet/fsharp/issues/18841), [PR #19811](https://github.com/dotnet/fsharp/pull/19811)) +* Honor `--nowarn` and `--warnaserror` for warnings emitted during command-line option parsing ([Issue #19576](https://github.com/dotnet/fsharp/issues/19576), [PR #19776](https://github.com/dotnet/fsharp/pull/19776)) +* Fix `[]` prefix attributes being silently dropped on class members, and fix false-positive `AllowMultiple=false` errors when `[]` and `[]` are applied to the same binding. ([Issue #17904](https://github.com/dotnet/fsharp/issues/17904), [Issue #19020](https://github.com/dotnet/fsharp/issues/19020), [PR #19738](https://github.com/dotnet/fsharp/pull/19738)) * 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 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)) * Fix NRE when calling virtual Object methods on value types through inline SRTP functions. ([Issue #8098](https://github.com/dotnet/fsharp/issues/8098), [PR #19511](https://github.com/dotnet/fsharp/pull/19511)) +* Narrow overload-resolution error ranges to the method name only instead of covering the entire expression. ([Issue #14284](https://github.com/dotnet/fsharp/issues/14284), [PR #19505](https://github.com/dotnet/fsharp/pull/19505)) * Fix attributes not resolved from opened namespaces in `namespace rec` / `module rec` scopes. ([Issue #7931](https://github.com/dotnet/fsharp/issues/7931), [PR #19502](https://github.com/dotnet/fsharp/pull/19502)) * Fix DU case names matching IWSAM member names no longer cause duplicate property entries. (Issue [#14321](https://github.com/dotnet/fsharp/issues/14321), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) * Fix DefaultAugmentation(false) duplicate entry in method table. (Issue [#16565](https://github.com/dotnet/fsharp/issues/16565), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) @@ -40,7 +46,7 @@ * Fix signature generation: backtick escaping for identifiers containing backticks. ([Issue #15389](https://github.com/dotnet/fsharp/issues/15389), [PR #19586](https://github.com/dotnet/fsharp/pull/19586)) * Fix signature generation: `private` keyword placement for prefix-style type abbreviations. ([Issue #15560](https://github.com/dotnet/fsharp/issues/15560), [PR #19586](https://github.com/dotnet/fsharp/pull/19586)) * Fix signature generation: missing `[]` attribute for types without visible constructors. ([Issue #16531](https://github.com/dotnet/fsharp/issues/16531), [PR #19586](https://github.com/dotnet/fsharp/pull/19586)) -* Fix F# exception serialization now preserves fields. (Issue [#878](https://github.com/dotnet/fsharp/issues/878), [PR #19342](https://github.com/dotnet/fsharp/pull/19342)) +* Fix F# exception serialization now preserves fields (gated behind `--langversion:11`). (Issue [#878](https://github.com/dotnet/fsharp/issues/878), [PR #19342](https://github.com/dotnet/fsharp/pull/19342), [PR #19746](https://github.com/dotnet/fsharp/pull/19746)) * Fix methods being tagged as `Member` instead of `Method` in tooltips. ([Issue #10540](https://github.com/dotnet/fsharp/issues/10540), [PR #19507](https://github.com/dotnet/fsharp/pull/19507)) * Fix Debug-mode compilation when mixing resumable and standard computation expressions. ([Issue #19625](https://github.com/dotnet/fsharp/issues/19625), [PR #19630](https://github.com/dotnet/fsharp/pull/19630)) * IlxGen: fix missing CompilationMapping attribute for generic values ([PR #19643](https://github.com/dotnet/fsharp/pull/19643)) @@ -52,17 +58,26 @@ * Fix signature generation: SRTP constraints use postfix syntax that fails conformance, now uses explicit type param declarations. ([Issue #19594](https://github.com/dotnet/fsharp/issues/19594), [PR #19609](https://github.com/dotnet/fsharp/pull/19609)) * Fix signature generation: type params with special characters missing backtick escaping. ([Issue #19595](https://github.com/dotnet/fsharp/issues/19595), [PR #19609](https://github.com/dotnet/fsharp/pull/19609)) * Fix internal error when using custom attribute with `[]` value type parameter and no `[]`. ([Issue #8353](https://github.com/dotnet/fsharp/issues/8353), [PR #19484](https://github.com/dotnet/fsharp/pull/19484)) +* Fix overload resolution of static member extension if one or more intrinsics candidates exist ([Issue #19664](https://github.com/dotnet/fsharp/issues/19664), [PR #19698](https://github.com/dotnet/fsharp/pull/19698)) * Fix parallel compilation of scripts ([PR #19649](https://github.com/dotnet/fsharp/pull/19649)) * Fix parser recovery, name resolution, and code completion for unfinished enum patterns ([PR #19708](https://github.com/dotnet/fsharp/pull/19708)) * Parser: fix unexpected diagnostics in debug builds, improve error messages ([PR #19730](https://github.com/dotnet/fsharp/pull/19730)) +* Allow `| null` nullable annotation on a `[]` over a reference type (e.g. the FSharp.UMX `type string<[] 'm> = string` pattern). ([Issue #19657](https://github.com/dotnet/fsharp/issues/19657)) +* Fix `[] ?param` optional parameters could not be passed using the explicit `?param = expr` caller-side syntax with a `ValueOption` value. ([Issue #19711](https://github.com/dotnet/fsharp/issues/19711), [PR #19742](https://github.com/dotnet/fsharp/pull/19742)) +* Fix signature conformance: overloaded member with unit parameter `M(())` now matches sig `member M: unit -> unit`. ([Issue #19596](https://github.com/dotnet/fsharp/issues/19596), [PR #19615](https://github.com/dotnet/fsharp/pull/19615)) +* Reference assembly MVIDs are now deterministic across compiler invocations. Previously, `--refout` / `true` produced a different MVID every build because the implied signature hash used .NET's randomized `String.GetHashCode()`. ([Issue #19751](https://github.com/dotnet/fsharp/issues/19751), [PR #19801](https://github.com/dotnet/fsharp/pull/19801)) +* Parser: recover on unfinished if and binary expressions +([PR #19724](https://github.com/dotnet/fsharp/pull/19724)) ### Added * Added warning FS3884 when a function or delegate value is used as an interpolated string argument. ([PR #19289](https://github.com/dotnet/fsharp/pull/19289)) +* Symbols: add ObsoleteDiagnosticInfo ([PR #19359](https://github.com/dotnet/fsharp/pull/19359)) * Add `#version;;` directive to F# Interactive to display version and environment information. ([Issue #13307](https://github.com/dotnet/fsharp/issues/13307), [PR #19332](https://github.com/dotnet/fsharp/pull/19332)) ### Changed * Improvements in error and warning messages: new error FS3885 when `let!`/`use!` is the final expression in a computation expression; new warning FS3886 when a list literal contains a single tuple element (likely missing `;` separator); improved wording for FS0003, FS0025, FS0039, FS0072, FS0247, FS0597, FS0670, FS3082, and SRTP operator-not-in-scope hints. ([PR #19398](https://github.com/dotnet/fsharp/pull/19398)) +* Exception field serialization (`GetObjectData` and field-restoring constructor) is now gated behind `langversion:11` (`LanguageFeature.ExceptionFieldSerializationSupport`). With langversion ≤10, exception codegen is unchanged from pre-#19342 behavior. ([PR #19746](https://github.com/dotnet/fsharp/pull/19746)) ### Breaking Changes diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index 19c3291c8c7..21f6eba26cd 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1619,17 +1619,7 @@ let IlAssemblyCodeInstrHasEffect i = | EI_ilzero _ -> false | _ -> true -let tyargsContainFreeTypars tyargs = - match tyargs with - | [] -> false - | _ -> not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars) - -let ILAsmHasEffect instrs tyargs = - instrs |> List.exists (function - // EI_ilzero<'T> embeds the type into IL; eliminating the expression would orphan - // free typars referenced only through tyargs and trip FS0073 in IlxGen. - | EI_ilzero _ -> tyargsContainFreeTypars tyargs - | i -> IlAssemblyCodeInstrHasEffect i) +let IlAssemblyCodeHasEffect instrs = List.exists IlAssemblyCodeInstrHasEffect instrs let rec ExprHasEffect g expr = match stripDebugPoints expr with @@ -1640,7 +1630,7 @@ let rec ExprHasEffect g expr = | Expr.Const _ -> false // type applications do not have effects, with the exception of type functions | Expr.App (f0, _, _, [], _) -> IsTyFuncValRefExpr f0 || ExprHasEffect g f0 - | Expr.Op (op, tyargs, args, m) -> ExprsHaveEffect g args || OpHasEffect g m op tyargs + | Expr.Op (op, _, args, m) -> ExprsHaveEffect g args || OpHasEffect g m op | Expr.LetRec (binds, body, _, _) -> BindingsHaveEffect g binds || ExprHasEffect g body | Expr.Let (bind, body, _, _) -> BindingHasEffect g bind || ExprHasEffect g body // REVIEW: could add Expr.Obj on an interface type - these are similar to records of lambda expressions @@ -1652,7 +1642,7 @@ and BindingsHaveEffect g binds = List.exists (BindingHasEffect g) binds and BindingHasEffect g bind = bind.Expr |> ExprHasEffect g -and OpHasEffect g m op tyargs = +and OpHasEffect g m op = match op with | TOp.Tuple _ -> false | TOp.AnonRecd _ -> false @@ -1666,7 +1656,7 @@ and OpHasEffect g m op tyargs = | TOp.UnionCaseTagGet _ -> false | TOp.UnionCaseProof _ -> false | TOp.UnionCaseFieldGet (ucref, n) -> isUnionCaseFieldMutable g ucref n - | TOp.ILAsm (instrs, _) -> ILAsmHasEffect instrs tyargs + | TOp.ILAsm (instrs, _) -> IlAssemblyCodeHasEffect instrs | TOp.TupleFieldGet _ -> false | TOp.ExnFieldGet (ecref, n) -> isExnFieldMutable ecref n | TOp.RefAddrGet _ -> false @@ -2581,7 +2571,7 @@ and OptimizeExprOp cenv env (op, tyargs, args, m) = newExpr, { TotalSize = 1 FunctionSize = 1 - HasEffect = OpHasEffect g m newOp tyargs + HasEffect = OpHasEffect g m newOp MightMakeCriticalTailcall = false Info = ValueOfExpr newExpr } @@ -2653,7 +2643,7 @@ and OptimizeExprOpFallback cenv env (op, tyargs, argsR, m) arginfos value_ = let argEffects = OrEffects arginfos let argValues = List.map (fun x -> x.Info) arginfos - let effect = OpHasEffect g m op tyargs + let effect = OpHasEffect g m op let cost, value_ = match op with | TOp.UnionCase c -> 2, MakeValueInfoForUnionCase c (Array.ofList argValues) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs index 14522390249..af1008ad26b 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs @@ -219,9 +219,10 @@ let empty<'T> = Seq.empty<'T> """ ] - // https://github.com/dotnet/fsharp/issues/17775 + // https://github.com/dotnet/fsharp/issues/18128 + // Concrete-type Unchecked.defaultof bindings should be eliminated under optimization. [] - let ``Unchecked_defaultof_concrete_type_eliminated_when_unused`` () = + let ``Issue_18128_Unchecked_defaultof_concrete_eliminated`` () = FSharp """ module Test @@ -242,35 +243,31 @@ let f (n: float32) = |> verifyILNotPresent [ "initobj" ] |> ignore - // https://github.com/dotnet/fsharp/issues/17775 - // FS0073 regression: free type variable in EI_ilzero tyargs must not be eliminated. + // https://github.com/dotnet/fsharp/issues/18128 + // The real-world FSharpPlus-style SRTP witness pattern from the issue. + // Exercises Unchecked.defaultof<'T> and Unchecked.defaultof<^b> as nested inline witnesses. [] - let ``Unchecked_defaultof_typar_kept_in_SRTP_context`` () = + let ``Issue_18128_SRTP_witness_pattern_compiles_and_optimizes`` () = FSharp """ module Test -let inline f< ^T when ^T : (static member op_Explicit: ^T -> int)> (x: ^T) = - let _ = Unchecked.defaultof< ^T > - int x -""" - |> asLibrary - |> withOptimize - |> compile - |> shouldSucceed - |> ignore +open System.ComponentModel +open FSharp.Core.LanguagePrimitives - // https://github.com/dotnet/fsharp/issues/17775 - // Nested free typar (Wrapper< ^T >) must also keep the binding alive. - [] - let ``Unchecked_defaultof_nested_typar_kept_in_SRTP_context`` () = - FSharp """ -module Test +[] +type PreOps = + static member inline Double (n: float<'u>) : float<'u> = n * 2. + static member inline Double (n: float32<'u>) : float32<'u> = n * 2.f -type Wrapper<'T> = { Value: 'T } +#nowarn "64" +module PreludeOperators = + let inline private nil<'T> = Unchecked.defaultof<'T> + let inline double (x: ^a) = + let inline _call (_: ^M, input: ^I, _: ^R) = ((^M or ^I) : (static member Double : ^I -> ^R) input) + _call (nil, x, nil< ^b >) -let inline f< ^T when ^T : (static member op_Explicit: ^T -> int)> (x: ^T) = - let _ = Unchecked.defaultof> - int x +open PreludeOperators +let doWork (n: float) = double n """ |> asLibrary |> withOptimize @@ -278,9 +275,9 @@ let inline f< ^T when ^T : (static member op_Explicit: ^T -> int)> (x: ^T) = |> shouldSucceed |> ignore - // Non-regression: the EI_ilzero fix must not over-broadly mark other effect-free - // generic operations as effectful. A generic Some over a free typar should still - // be eliminated as a dead binding under optimization. + // Non-regression: making EI_ilzero effect-free must not be over-broadly applied to + // other effect-free generic operations. A generic Some over a free typar should + // continue to be eliminated as a dead binding. [] let ``Generic_Some_unused_binding_still_eliminated`` () = FSharp """ From 72b85a36c083f9b7689eb2f9343c0878909d976a Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Mon, 1 Jun 2026 13:29:56 +0200 Subject: [PATCH 06/16] Round 2 polish: stronger IL assertions, cctor-soundness test, drop jargon Per round-2 expert review: - Strengthen concrete-elimination test to also forbid any Decimal local slot (catches partial regressions where 1 of 3 bindings survives). - Pin the SRTP doWork body exactly to its eliminated form (ldarg + ldc.r8 + mul + ret); the previous shouldSucceed-only assertion did not catch optimizer regressions on the witness path. - Add a cctor-soundness test documenting that defaultof emits ldnull (not newobj), so eliminating the binding cannot suppress an observable cctor side-effect that previously occurred. f's body is pinned to its 2-instruction post-optimisation form. - Drop the Generic_Some test - it passed on main without this PR, so it was not a regression catcher for the EI_ilzero change. - Reword the release-notes entry to drop the internal opcode name and describe the user-visible effect. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/11.0.100.md | 2 +- .../CodeGenRegressions_Observations.fs | 49 ++++++++++++++----- 2 files changed, 37 insertions(+), 14 deletions(-) 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 e1f921c04dd..fd15350a10a 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,6 +1,6 @@ ### Fixed -* Optimizer: treat `Unchecked.defaultof<'T>` (`EI_ilzero`) as effect-free so unused bindings (e.g. SRTP witness arguments) are eliminated under optimization. ([Issue #18128](https://github.com/dotnet/fsharp/issues/18128), [PR #19758](https://github.com/dotnet/fsharp/pull/19758)) +* Unused `Unchecked.defaultof<'T>` bindings are now eliminated under optimization. This removes redundant `initobj`/`ldnull;pop` sequences emitted by SRTP helpers that use `Unchecked.defaultof` as dummy arguments to drive static-member-constraint resolution. ([Issue #18128](https://github.com/dotnet/fsharp/issues/18128), [PR #19758](https://github.com/dotnet/fsharp/pull/19758)) * Reject non-function bindings for single-case and partial active pattern names with FS1209, matching the existing multi-case behavior. ([PR #19763](https://github.com/dotnet/fsharp/pull/19763)) * Fix FS0421 "The address of the variable cannot be used at this point" incorrectly raised for the discard pattern `let _ = &expr` when `let x = &expr` compiles. ([Issue #18841](https://github.com/dotnet/fsharp/issues/18841), [PR #19811](https://github.com/dotnet/fsharp/pull/19811)) * Honor `--nowarn` and `--warnaserror` for warnings emitted during command-line option parsing ([Issue #19576](https://github.com/dotnet/fsharp/issues/19576), [PR #19776](https://github.com/dotnet/fsharp/pull/19776)) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs index af1008ad26b..d8cc1ee56f5 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs @@ -221,6 +221,8 @@ let empty<'T> = Seq.empty<'T> // https://github.com/dotnet/fsharp/issues/18128 // Concrete-type Unchecked.defaultof bindings should be eliminated under optimization. + // Pins both the absence of initobj and that no decimal local slot is allocated for + // any of the three discarded bindings. [] let ``Issue_18128_Unchecked_defaultof_concrete_eliminated`` () = FSharp """ @@ -240,12 +242,13 @@ let f (n: float32) = |> withOptimize |> compile |> shouldSucceed - |> verifyILNotPresent [ "initobj" ] + |> verifyILNotPresent [ "initobj"; "valuetype [runtime]System.Decimal" ] |> ignore // https://github.com/dotnet/fsharp/issues/18128 - // The real-world FSharpPlus-style SRTP witness pattern from the issue. - // Exercises Unchecked.defaultof<'T> and Unchecked.defaultof<^b> as nested inline witnesses. + // The real-world FSharpPlus-style SRTP witness pattern from the issue. After elimination, + // doWork reduces to a direct double-precision multiplication; the nil and nil<^b> + // witness bindings are gone. [] let ``Issue_18128_SRTP_witness_pattern_compiles_and_optimizes`` () = FSharp """ @@ -273,26 +276,46 @@ let doWork (n: float) = double n |> withOptimize |> compile |> shouldSucceed + |> verifyIL [ + """.method public static float64 doWork(float64 n) cil managed + { + + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldc.r8 2 + IL_000a: mul + IL_000b: ret + }""" + ] |> ignore - // Non-regression: making EI_ilzero effect-free must not be over-broadly applied to - // other effect-free generic operations. A generic Some over a free typar should - // continue to be eliminated as a dead binding. + // Soundness: eliminating an unused Unchecked.defaultof binding must not introduce + // a cctor trigger that the unoptimised code did not have. defaultof of a reference type + // emits ldnull (never newobj), so f's body reduces cleanly to the return constant with + // no reference to WithCctor. [] - let ``Generic_Some_unused_binding_still_eliminated`` () = + let ``Issue_18128_eliminated_defaultof_does_not_run_cctor`` () = FSharp """ module Test -open System +type WithCctor() = + static do failwith "cctor must not run" -let f<'T> (x: 'T) = - Console.WriteLine "before" - let _ = Some x - Console.WriteLine "after" +let f () = + let _ = Unchecked.defaultof + 42 """ |> asLibrary |> withOptimize |> compile |> shouldSucceed - |> verifyILNotPresent [ "FSharpOption" ] + |> verifyIL [ + """.method public static int32 f() cil managed + { + + .maxstack 8 + IL_0000: ldc.i4.s 42 + IL_0002: ret + }""" + ] |> ignore From 2c7b57f5b8326fa481f70c567fcf8e65bc58de68 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Mon, 1 Jun 2026 13:35:21 +0200 Subject: [PATCH 07/16] Round 3 polish: drop stray blank line, rename cctor test for accuracy Per round-3 expert review: - Drop the stray blank line at Optimizer.fs:2645; final functional diff vs main is now exactly one line. - Rename the cctor-soundness test from 'does_not_run_cctor' to 'leaves_no_reference_to_T_in_caller'. The previous name implied a runtime guarantee, but the assertion is purely IL-shape: it verifies that f's body contains no reference to WithCctor (defaultof of a reference type lowers to ldnull, so the cctor was never reachable through f to begin with). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Optimize/Optimizer.fs | 1 - .../CodeGenRegressions_Observations.fs | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index 21f6eba26cd..e7a06b3b6b3 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -2642,7 +2642,6 @@ and OptimizeExprOpFallback cenv env (op, tyargs, argsR, m) arginfos value_ = let argsFSize = AddFunctionSizes arginfos let argEffects = OrEffects arginfos let argValues = List.map (fun x -> x.Info) arginfos - let effect = OpHasEffect g m op let cost, value_ = match op with diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs index d8cc1ee56f5..9657c20195a 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs @@ -289,12 +289,12 @@ let doWork (n: float) = double n ] |> ignore - // Soundness: eliminating an unused Unchecked.defaultof binding must not introduce - // a cctor trigger that the unoptimised code did not have. defaultof of a reference type - // emits ldnull (never newobj), so f's body reduces cleanly to the return constant with - // no reference to WithCctor. + // Soundness pin: optimizing away an unused `Unchecked.defaultof` must not introduce + // a new reference to T in the enclosing method. `defaultof` of a reference type lowers + // to `ldnull` (not `newobj`), so the binding's removal cannot suppress an observable + // cctor call - f's body should contain no reference to WithCctor at all. [] - let ``Issue_18128_eliminated_defaultof_does_not_run_cctor`` () = + let ``Issue_18128_eliminated_defaultof_leaves_no_reference_to_T_in_caller`` () = FSharp """ module Test From f293050476fe6e11f8cbc462bbd92f5f07821f82 Mon Sep 17 00:00:00 2001 From: Copilot Date: Wed, 3 Jun 2026 13:51:57 +0200 Subject: [PATCH 08/16] Restore tyargsContainFreeTypars guard for EI_ilzero; fix IL test format Two CI failures addressed: 1. FS0073 in FSharpPlus regression tests: The previous commit stripped the tyargsContainFreeTypars protection, claiming no reproducer exists. FSharpPlus NET10 builds prove otherwise - SRTP witness patterns like nil<^b> where the type variable remains unsolved at optimization time need the binding kept. Restored via ILAsmWithIlzeroHasEffect: when EI_ilzero instructions are present and tyargs contain free type parameters, treat the ILAsm as effectful. 2. IL format mismatch in SRTP test: Different CI configurations (NoRealsig, CompressedMetadata) output ldc.r8 2. instead of ldc.r8 2. Changed the test from exact IL body matching to verifyILNotPresent which tests the actual optimization behavior (absence of initobj/ldnull) without sensitivity to float literal formatting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Optimize/Optimizer.fs | 23 +++++++++++++++---- .../CodeGenRegressions_Observations.fs | 12 +--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index e7a06b3b6b3..714fea2934c 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1621,6 +1621,19 @@ let IlAssemblyCodeInstrHasEffect i = let IlAssemblyCodeHasEffect instrs = List.exists IlAssemblyCodeInstrHasEffect instrs +/// EI_ilzero embeds tyargs into IL; if those tyargs contain free type parameters +/// (e.g. SRTP witnesses like nil<^b>), eliminating the binding would orphan +/// the type vars and trip FS0073 in IlxGen. Treat as effectful in that case. +let ILAsmWithIlzeroHasEffect instrs tyargs = + let hasIlzero = instrs |> List.exists (function EI_ilzero _ -> true | _ -> false) + + if hasIlzero then + match tyargs with + | [] -> false + | _ -> not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars) + else + IlAssemblyCodeHasEffect instrs + let rec ExprHasEffect g expr = match stripDebugPoints expr with | Expr.Val (vref, _, _) -> vref.IsTypeFunction || vref.IsMutable @@ -1630,7 +1643,7 @@ let rec ExprHasEffect g expr = | Expr.Const _ -> false // type applications do not have effects, with the exception of type functions | Expr.App (f0, _, _, [], _) -> IsTyFuncValRefExpr f0 || ExprHasEffect g f0 - | Expr.Op (op, _, args, m) -> ExprsHaveEffect g args || OpHasEffect g m op + | Expr.Op (op, tyargs, args, m) -> ExprsHaveEffect g args || OpHasEffect g m op tyargs | Expr.LetRec (binds, body, _, _) -> BindingsHaveEffect g binds || ExprHasEffect g body | Expr.Let (bind, body, _, _) -> BindingHasEffect g bind || ExprHasEffect g body // REVIEW: could add Expr.Obj on an interface type - these are similar to records of lambda expressions @@ -1642,7 +1655,7 @@ and BindingsHaveEffect g binds = List.exists (BindingHasEffect g) binds and BindingHasEffect g bind = bind.Expr |> ExprHasEffect g -and OpHasEffect g m op = +and OpHasEffect g m op tyargs = match op with | TOp.Tuple _ -> false | TOp.AnonRecd _ -> false @@ -1656,7 +1669,7 @@ and OpHasEffect g m op = | TOp.UnionCaseTagGet _ -> false | TOp.UnionCaseProof _ -> false | TOp.UnionCaseFieldGet (ucref, n) -> isUnionCaseFieldMutable g ucref n - | TOp.ILAsm (instrs, _) -> IlAssemblyCodeHasEffect instrs + | TOp.ILAsm (instrs, _) -> ILAsmWithIlzeroHasEffect instrs tyargs | TOp.TupleFieldGet _ -> false | TOp.ExnFieldGet (ecref, n) -> isExnFieldMutable ecref n | TOp.RefAddrGet _ -> false @@ -2571,7 +2584,7 @@ and OptimizeExprOp cenv env (op, tyargs, args, m) = newExpr, { TotalSize = 1 FunctionSize = 1 - HasEffect = OpHasEffect g m newOp + HasEffect = OpHasEffect g m newOp tyargs MightMakeCriticalTailcall = false Info = ValueOfExpr newExpr } @@ -2642,7 +2655,7 @@ and OptimizeExprOpFallback cenv env (op, tyargs, argsR, m) arginfos value_ = let argsFSize = AddFunctionSizes arginfos let argEffects = OrEffects arginfos let argValues = List.map (fun x -> x.Info) arginfos - let effect = OpHasEffect g m op + let effect = OpHasEffect g m op tyargs let cost, value_ = match op with | TOp.UnionCase c -> 2, MakeValueInfoForUnionCase c (Array.ofList argValues) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs index 9657c20195a..90981fbee1b 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs @@ -276,17 +276,7 @@ let doWork (n: float) = double n |> withOptimize |> compile |> shouldSucceed - |> verifyIL [ - """.method public static float64 doWork(float64 n) cil managed - { - - .maxstack 8 - IL_0000: ldarg.0 - IL_0001: ldc.r8 2 - IL_000a: mul - IL_000b: ret - }""" - ] + |> verifyILNotPresent [ "initobj"; "ldnull" ] |> ignore // Soundness pin: optimizing away an unused `Unchecked.defaultof` must not introduce From 8708a42676b56444763a99499079258e68c9302f Mon Sep 17 00:00:00 2001 From: Copilot Date: Wed, 3 Jun 2026 14:22:55 +0200 Subject: [PATCH 09/16] Simplify ILAsmWithIlzeroHasEffect to also check other instrs in list The previous if/else structure would skip the IlAssemblyCodeHasEffect check entirely when EI_ilzero was present. Rewrite to always check the standard instruction-level effects, then additionally flag the free-typar ilzero guard. This is more robust if EI_ilzero ever appears alongside other instructions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Optimize/Optimizer.fs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index 714fea2934c..cb1a8e564bf 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1625,14 +1625,12 @@ let IlAssemblyCodeHasEffect instrs = List.exists IlAssemblyCodeInstrHasEffect in /// (e.g. SRTP witnesses like nil<^b>), eliminating the binding would orphan /// the type vars and trip FS0073 in IlxGen. Treat as effectful in that case. let ILAsmWithIlzeroHasEffect instrs tyargs = - let hasIlzero = instrs |> List.exists (function EI_ilzero _ -> true | _ -> false) + let ilzeroHasFreeTypars = + instrs |> List.exists (function EI_ilzero _ -> true | _ -> false) + && not (List.isEmpty tyargs) + && not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars) - if hasIlzero then - match tyargs with - | [] -> false - | _ -> not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars) - else - IlAssemblyCodeHasEffect instrs + IlAssemblyCodeHasEffect instrs || ilzeroHasFreeTypars let rec ExprHasEffect g expr = match stripDebugPoints expr with From 7d8b06df41ab8b2199f787660dfa8d8818ade5db Mon Sep 17 00:00:00 2001 From: Copilot Date: Wed, 3 Jun 2026 17:05:22 +0200 Subject: [PATCH 10/16] Fix EI_ilzero effect check: require concrete TType_app (not TType_var) The previous check used freeInTypes which follows type parameter solutions via stripTyparEqns. This could mark EI_ilzero as effect-free when the tyarg was a solved SRTP type variable (e.g. ^a = SomeType), because freeInTypes would see the solution as concrete. However, in complex SRTP scenarios (like FSharpPlus operators), intermediate anonymous type variables referenced through solved SRTPs can trip FS0073 in IlxGen when the binding is eliminated. The fix checks the tree-level type node directly: only TType_app, TType_tuple, etc. pass the safety check. Any TType_var (even solved ones) is conservatively treated as effectful, preventing elimination. This still enables the optimization for the common case: let _ = Unchecked.defaultof // eliminated (TType_app) let _ = Unchecked.defaultof // eliminated (TType_app) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Optimize/Optimizer.fs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index cb1a8e564bf..dacc4546ad9 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1621,16 +1621,26 @@ let IlAssemblyCodeInstrHasEffect i = let IlAssemblyCodeHasEffect instrs = List.exists IlAssemblyCodeInstrHasEffect instrs -/// EI_ilzero embeds tyargs into IL; if those tyargs contain free type parameters -/// (e.g. SRTP witnesses like nil<^b>), eliminating the binding would orphan -/// the type vars and trip FS0073 in IlxGen. Treat as effectful in that case. +/// EI_ilzero embeds tyargs into IL; eliminating the binding is only safe when +/// the tyargs are fully concrete types (not type variables, even solved ones). +/// Solved SRTP type variables can still reference intermediate anonymous typars +/// that trip FS0073 in IlxGen when the binding is removed. let ILAsmWithIlzeroHasEffect instrs tyargs = - let ilzeroHasFreeTypars = - instrs |> List.exists (function EI_ilzero _ -> true | _ -> false) - && not (List.isEmpty tyargs) - && not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars) + let hasIlzero = instrs |> List.exists (function EI_ilzero _ -> true | _ -> false) - IlAssemblyCodeHasEffect instrs || ilzeroHasFreeTypars + if not hasIlzero then + IlAssemblyCodeHasEffect instrs + else + // EI_ilzero is effect-free only when ALL tyargs are concrete TType_app/TType_tuple + // (not TType_var at the tree node level, regardless of whether the typar is solved). + let ilzeroIsSafe = + not (List.isEmpty tyargs) + && tyargs |> List.forall (fun ty -> match ty with TType_var _ -> false | _ -> true) + + let otherInstrsHaveEffect = + instrs |> List.exists (fun i -> match i with EI_ilzero _ -> false | _ -> IlAssemblyCodeInstrHasEffect i) + + otherInstrsHaveEffect || not ilzeroIsSafe let rec ExprHasEffect g expr = match stripDebugPoints expr with From 44d01ebb9ee6d32f9abf64c11ca05e7d523446b9 Mon Sep 17 00:00:00 2001 From: Copilot Date: Mon, 29 Jun 2026 08:00:24 +0200 Subject: [PATCH 11/16] Refine ilzero effect-freedom to fully-ground types; add tests Eliminate Unchecked.defaultof bindings only when tyargs have no free typars, fixing FS0073 in SRTP-heavy code (FSharpPlus). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Optimize/Optimizer.fs | 12 +++-- .../UncheckedDefaultofOptimization.fs | 44 +++++++++++++++++++ .../FSharp.Compiler.ComponentTests.fsproj | 1 + 3 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index 590d88a4a25..2603f50f99e 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1654,20 +1654,18 @@ let IlAssemblyCodeInstrHasEffect i = let IlAssemblyCodeHasEffect instrs = List.exists IlAssemblyCodeInstrHasEffect instrs /// EI_ilzero embeds tyargs into IL; eliminating the binding is only safe when -/// the tyargs are fully concrete types (not type variables, even solved ones). -/// Solved SRTP type variables can still reference intermediate anonymous typars -/// that trip FS0073 in IlxGen when the binding is removed. +/// the tyargs are fully ground (contain no free type variables, even after +/// following solutions). A binding whose type still references unsolved typars +/// is the only thing pinning those typars; removing it leaves them unsolved and +/// trips FS0073 in IlxGen. This is common for SRTP witness/dummy arguments. let ILAsmWithIlzeroHasEffect instrs tyargs = let hasIlzero = instrs |> List.exists (function EI_ilzero _ -> true | _ -> false) if not hasIlzero then IlAssemblyCodeHasEffect instrs else - // EI_ilzero is effect-free only when ALL tyargs are concrete TType_app/TType_tuple - // (not TType_var at the tree node level, regardless of whether the typar is solved). let ilzeroIsSafe = - not (List.isEmpty tyargs) - && tyargs |> List.forall (fun ty -> match ty with TType_var _ -> false | _ -> true) + tyargs |> List.forall (fun ty -> Zset.isEmpty (freeInType CollectTypars ty).FreeTypars) let otherInstrsHaveEffect = instrs |> List.exists (fun i -> match i with EI_ilzero _ -> false | _ -> IlAssemblyCodeInstrHasEffect i) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs new file mode 100644 index 00000000000..b1ec7f8a017 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace EmittedIL + +open Xunit +open FSharp.Test.Compiler + +module ``UncheckedDefaultofOptimization`` = + + [] + let ``Unused Unchecked.defaultof bindings of concrete types are eliminated`` () = + FSharp """ +module Test +open System +let f (n: float32) = + Console.WriteLine n + let _ = Unchecked.defaultof + let _ = Unchecked.defaultof + let _ = Unchecked.defaultof + let n' = n * 2.f + Console.WriteLine n' + """ + |> withOptimize + |> asLibrary + |> compile + |> shouldSucceed + |> verifyILNotPresent ["initobj [runtime]System.Decimal"] + + [] + let ``Unused Unchecked.defaultof bindings of unsolved generic types do not cause FS0073`` () = + // Regression for SRTP witness/dummy-argument patterns (e.g. FSharpPlus) where + // eliminating an ilzero binding referencing unsolved typars trips FS0073 in IlxGen. + FSharp """ +module Test +let inline witness () = Unchecked.defaultof<'T> +let inline run< ^T when ^T: (static member Zero: ^T)> () = + let _ = (witness () : ^T) + LanguagePrimitives.GenericZero< ^T> +let v : int = run< int> () + """ + |> withOptimize + |> asLibrary + |> compile + |> shouldSucceed diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 2f96624917e..d626972831d 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -250,6 +250,7 @@ + From 1bcc1c20199e38bd6959523e5fee8518c04655fb Mon Sep 17 00:00:00 2001 From: Copilot Date: Mon, 29 Jun 2026 09:39:44 +0200 Subject: [PATCH 12/16] Revert defaultof elimination: unsafe for SRTP witness typars The optimizer cannot determine per-op whether eliminating an EI_ilzero (Unchecked.defaultof) binding is safe: even ground-tyarg bindings can pin sibling SRTP typars resolved later, causing FS0073 in IlxGen on FSharpPlus monad CEs. Revert to main behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/11.0.100.md | 1 - src/Compiler/Optimize/Optimizer.fs | 40 ++------ .../CodeGenRegressions_Observations.fs | 91 ------------------- .../UncheckedDefaultofOptimization.fs | 44 --------- .../FSharp.Compiler.ComponentTests.fsproj | 1 - 5 files changed, 10 insertions(+), 167 deletions(-) delete mode 100644 tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs 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 9bb8558dbfc..ba843ca702c 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,6 +1,5 @@ ### Fixed -* Unused `Unchecked.defaultof<'T>` bindings are now eliminated under optimization. This removes redundant `initobj`/`ldnull;pop` sequences emitted by SRTP helpers that use `Unchecked.defaultof` as dummy arguments to drive static-member-constraint resolution. ([Issue #18128](https://github.com/dotnet/fsharp/issues/18128), [PR #19758](https://github.com/dotnet/fsharp/pull/19758)) * Semantic classification no longer marks recursive object self-references (`as this`, `let rec` self-refs) as mutable. ([Issue #5229](https://github.com/dotnet/fsharp/issues/5229)) * Fix `MethodAccessException` under `--realsig+` when a closure (inner `let rec`, `task`/`async` state machine, or quotation splice) inside a member defined in an intrinsic type augmentation (`type C with member ...`) accesses a `private` member of `C`. The synthesized closure is now nested inside the declaring type instead of beside it in the module class. ([Issue #19933](https://github.com/dotnet/fsharp/issues/19933), [PR #19955](https://github.com/dotnet/fsharp/pull/19955)) * Preserve source range for type errors on empty-bodied computation expressions (e.g. `foo {}`) in pipelines, function arguments, and type-annotated contexts, instead of reporting `unknown(1,1)`. ([Issue #19550](https://github.com/dotnet/fsharp/issues/19550), [PR #19849](https://github.com/dotnet/fsharp/pull/19849)) diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index 2603f50f99e..ae71bf0811f 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1644,34 +1644,14 @@ let SplitValuesByIsUsedOrHasEffect cenv fvs x = let IlAssemblyCodeInstrHasEffect i = match i with - | AI_nop | AI_ldc _ | AI_add | AI_sub | AI_mul | AI_xor | AI_and | AI_or - | AI_ceq | AI_cgt | AI_cgt_un | AI_clt | AI_clt_un | AI_conv _ | AI_shl - | AI_shr | AI_shr_un | AI_neg | AI_not | AI_ldnull - | I_ldstr _ | I_ldtoken _ - | EI_ilzero _ -> false + | ( AI_nop | AI_ldc _ | AI_add | AI_sub | AI_mul | AI_xor | AI_and | AI_or + | AI_ceq | AI_cgt | AI_cgt_un | AI_clt | AI_clt_un | AI_conv _ | AI_shl + | AI_shr | AI_shr_un | AI_neg | AI_not | AI_ldnull ) + | I_ldstr _ | I_ldtoken _ -> false | _ -> true - + let IlAssemblyCodeHasEffect instrs = List.exists IlAssemblyCodeInstrHasEffect instrs -/// EI_ilzero embeds tyargs into IL; eliminating the binding is only safe when -/// the tyargs are fully ground (contain no free type variables, even after -/// following solutions). A binding whose type still references unsolved typars -/// is the only thing pinning those typars; removing it leaves them unsolved and -/// trips FS0073 in IlxGen. This is common for SRTP witness/dummy arguments. -let ILAsmWithIlzeroHasEffect instrs tyargs = - let hasIlzero = instrs |> List.exists (function EI_ilzero _ -> true | _ -> false) - - if not hasIlzero then - IlAssemblyCodeHasEffect instrs - else - let ilzeroIsSafe = - tyargs |> List.forall (fun ty -> Zset.isEmpty (freeInType CollectTypars ty).FreeTypars) - - let otherInstrsHaveEffect = - instrs |> List.exists (fun i -> match i with EI_ilzero _ -> false | _ -> IlAssemblyCodeInstrHasEffect i) - - otherInstrsHaveEffect || not ilzeroIsSafe - let rec ExprHasEffect g expr = match stripDebugPoints expr with | Expr.Val (vref, _, _) -> vref.IsTypeFunction || vref.IsMutable @@ -1681,7 +1661,7 @@ let rec ExprHasEffect g expr = | Expr.Const _ -> false // type applications do not have effects, with the exception of type functions | Expr.App (f0, _, _, [], _) -> IsTyFuncValRefExpr f0 || ExprHasEffect g f0 - | Expr.Op (op, tyargs, args, m) -> ExprsHaveEffect g args || OpHasEffect g m op tyargs + | Expr.Op (op, _, args, m) -> ExprsHaveEffect g args || OpHasEffect g m op | Expr.LetRec (binds, body, _, _) -> BindingsHaveEffect g binds || ExprHasEffect g body | Expr.Let (bind, body, _, _) -> BindingHasEffect g bind || ExprHasEffect g body // REVIEW: could add Expr.Obj on an interface type - these are similar to records of lambda expressions @@ -1693,7 +1673,7 @@ and BindingsHaveEffect g binds = List.exists (BindingHasEffect g) binds and BindingHasEffect g bind = bind.Expr |> ExprHasEffect g -and OpHasEffect g m op tyargs = +and OpHasEffect g m op = match op with | TOp.Tuple _ -> false | TOp.AnonRecd _ -> false @@ -1707,7 +1687,7 @@ and OpHasEffect g m op tyargs = | TOp.UnionCaseTagGet _ -> false | TOp.UnionCaseProof _ -> false | TOp.UnionCaseFieldGet (ucref, n) -> isUnionCaseFieldMutable g ucref n - | TOp.ILAsm (instrs, _) -> ILAsmWithIlzeroHasEffect instrs tyargs + | TOp.ILAsm (instrs, _) -> IlAssemblyCodeHasEffect instrs | TOp.TupleFieldGet _ -> false | TOp.ExnFieldGet (ecref, n) -> isExnFieldMutable ecref n | TOp.RefAddrGet _ -> false @@ -2638,7 +2618,7 @@ and OptimizeExprOp cenv env (op, tyargs, args, m) = newExpr, { TotalSize = 1 FunctionSize = 1 - HasEffect = OpHasEffect g m newOp tyargs + HasEffect = OpHasEffect g m newOp MightMakeCriticalTailcall = false Info = ValueOfExpr newExpr } @@ -2715,7 +2695,7 @@ and OptimizeExprOpFallback cenv env (op, tyargs, argsR, m) arginfos value_ = let argsFSize = AddFunctionSizes arginfos let argEffects = OrEffects arginfos let argValues = List.map (fun x -> x.Info) arginfos - let effect = OpHasEffect g m op tyargs + let effect = OpHasEffect g m op let cost, value_ = match op with | TOp.UnionCase c -> 2, MakeValueInfoForUnionCase c (Array.ofList argValues) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs index 90981fbee1b..4eb3dc57b98 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs @@ -218,94 +218,3 @@ let empty<'T> = Seq.empty<'T> .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 09 00 00 00 00 00 ) """ ] - - // https://github.com/dotnet/fsharp/issues/18128 - // Concrete-type Unchecked.defaultof bindings should be eliminated under optimization. - // Pins both the absence of initobj and that no decimal local slot is allocated for - // any of the three discarded bindings. - [] - let ``Issue_18128_Unchecked_defaultof_concrete_eliminated`` () = - FSharp """ -module Test - -open System - -let f (n: float32) = - Console.WriteLine n - let _ = Unchecked.defaultof - let _ = Unchecked.defaultof - let _ = Unchecked.defaultof - let n' = n * 2.f - Console.WriteLine n' -""" - |> asLibrary - |> withOptimize - |> compile - |> shouldSucceed - |> verifyILNotPresent [ "initobj"; "valuetype [runtime]System.Decimal" ] - |> ignore - - // https://github.com/dotnet/fsharp/issues/18128 - // The real-world FSharpPlus-style SRTP witness pattern from the issue. After elimination, - // doWork reduces to a direct double-precision multiplication; the nil and nil<^b> - // witness bindings are gone. - [] - let ``Issue_18128_SRTP_witness_pattern_compiles_and_optimizes`` () = - FSharp """ -module Test - -open System.ComponentModel -open FSharp.Core.LanguagePrimitives - -[] -type PreOps = - static member inline Double (n: float<'u>) : float<'u> = n * 2. - static member inline Double (n: float32<'u>) : float32<'u> = n * 2.f - -#nowarn "64" -module PreludeOperators = - let inline private nil<'T> = Unchecked.defaultof<'T> - let inline double (x: ^a) = - let inline _call (_: ^M, input: ^I, _: ^R) = ((^M or ^I) : (static member Double : ^I -> ^R) input) - _call (nil, x, nil< ^b >) - -open PreludeOperators -let doWork (n: float) = double n -""" - |> asLibrary - |> withOptimize - |> compile - |> shouldSucceed - |> verifyILNotPresent [ "initobj"; "ldnull" ] - |> ignore - - // Soundness pin: optimizing away an unused `Unchecked.defaultof` must not introduce - // a new reference to T in the enclosing method. `defaultof` of a reference type lowers - // to `ldnull` (not `newobj`), so the binding's removal cannot suppress an observable - // cctor call - f's body should contain no reference to WithCctor at all. - [] - let ``Issue_18128_eliminated_defaultof_leaves_no_reference_to_T_in_caller`` () = - FSharp """ -module Test - -type WithCctor() = - static do failwith "cctor must not run" - -let f () = - let _ = Unchecked.defaultof - 42 -""" - |> asLibrary - |> withOptimize - |> compile - |> shouldSucceed - |> verifyIL [ - """.method public static int32 f() cil managed - { - - .maxstack 8 - IL_0000: ldc.i4.s 42 - IL_0002: ret - }""" - ] - |> ignore diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs deleted file mode 100644 index b1ec7f8a017..00000000000 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. - -namespace EmittedIL - -open Xunit -open FSharp.Test.Compiler - -module ``UncheckedDefaultofOptimization`` = - - [] - let ``Unused Unchecked.defaultof bindings of concrete types are eliminated`` () = - FSharp """ -module Test -open System -let f (n: float32) = - Console.WriteLine n - let _ = Unchecked.defaultof - let _ = Unchecked.defaultof - let _ = Unchecked.defaultof - let n' = n * 2.f - Console.WriteLine n' - """ - |> withOptimize - |> asLibrary - |> compile - |> shouldSucceed - |> verifyILNotPresent ["initobj [runtime]System.Decimal"] - - [] - let ``Unused Unchecked.defaultof bindings of unsolved generic types do not cause FS0073`` () = - // Regression for SRTP witness/dummy-argument patterns (e.g. FSharpPlus) where - // eliminating an ilzero binding referencing unsolved typars trips FS0073 in IlxGen. - FSharp """ -module Test -let inline witness () = Unchecked.defaultof<'T> -let inline run< ^T when ^T: (static member Zero: ^T)> () = - let _ = (witness () : ^T) - LanguagePrimitives.GenericZero< ^T> -let v : int = run< int> () - """ - |> withOptimize - |> asLibrary - |> compile - |> shouldSucceed diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index d626972831d..2f96624917e 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -250,7 +250,6 @@ - From 12dadbf355398991f002e3c1275998d78e5664b1 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 30 Jun 2026 08:24:54 +0200 Subject: [PATCH 13/16] Revert "Revert defaultof elimination: unsafe for SRTP witness typars" This reverts commit 1bcc1c20199e38bd6959523e5fee8518c04655fb. --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Optimize/Optimizer.fs | 40 ++++++-- .../CodeGenRegressions_Observations.fs | 91 +++++++++++++++++++ .../UncheckedDefaultofOptimization.fs | 44 +++++++++ .../FSharp.Compiler.ComponentTests.fsproj | 1 + 5 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs 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 c482bb6cdab..c98b36b1d5e 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -2,6 +2,7 @@ * Recursive `inline` functions and members now emit a single clear error (FS3890) instead of a misleading FS1113/FS1114 optimizer cascade. ([Issue #17991](https://github.com/dotnet/fsharp/issues/17991), [PR #19803](https://github.com/dotnet/fsharp/pull/19803)) * Report `FS0037 Duplicate definition of type or module` at type-check time when two sibling modules in a `module rec` / `namespace rec` group share a name, instead of letting the duplicate slip through to IL emit where it surfaced as the cryptic `FS2014: duplicate entry . in type index table`. ([Issue #6694](https://github.com/dotnet/fsharp/issues/6694), [PR #19913](https://github.com/dotnet/fsharp/pull/19913)) +* Unused `Unchecked.defaultof<'T>` bindings are now eliminated under optimization. This removes redundant `initobj`/`ldnull;pop` sequences emitted by SRTP helpers that use `Unchecked.defaultof` as dummy arguments to drive static-member-constraint resolution. ([Issue #18128](https://github.com/dotnet/fsharp/issues/18128), [PR #19758](https://github.com/dotnet/fsharp/pull/19758)) * Semantic classification no longer marks recursive object self-references (`as this`, `let rec` self-refs) as mutable. ([Issue #5229](https://github.com/dotnet/fsharp/issues/5229)) * Fix `MethodAccessException` under `--realsig+` when a closure (inner `let rec`, `task`/`async` state machine, or quotation splice) inside a member defined in an intrinsic type augmentation (`type C with member ...`) accesses a `private` member of `C`. The synthesized closure is now nested inside the declaring type instead of beside it in the module class. ([Issue #19933](https://github.com/dotnet/fsharp/issues/19933), [PR #19955](https://github.com/dotnet/fsharp/pull/19955)) * Preserve source range for type errors on empty-bodied computation expressions (e.g. `foo {}`) in pipelines, function arguments, and type-annotated contexts, instead of reporting `unknown(1,1)`. ([Issue #19550](https://github.com/dotnet/fsharp/issues/19550), [PR #19849](https://github.com/dotnet/fsharp/pull/19849)) diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index 83f3089f6fb..f8efa30dafa 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1644,14 +1644,34 @@ let SplitValuesByIsUsedOrHasEffect cenv fvs x = let IlAssemblyCodeInstrHasEffect i = match i with - | ( AI_nop | AI_ldc _ | AI_add | AI_sub | AI_mul | AI_xor | AI_and | AI_or - | AI_ceq | AI_cgt | AI_cgt_un | AI_clt | AI_clt_un | AI_conv _ | AI_shl - | AI_shr | AI_shr_un | AI_neg | AI_not | AI_ldnull ) - | I_ldstr _ | I_ldtoken _ -> false + | AI_nop | AI_ldc _ | AI_add | AI_sub | AI_mul | AI_xor | AI_and | AI_or + | AI_ceq | AI_cgt | AI_cgt_un | AI_clt | AI_clt_un | AI_conv _ | AI_shl + | AI_shr | AI_shr_un | AI_neg | AI_not | AI_ldnull + | I_ldstr _ | I_ldtoken _ + | EI_ilzero _ -> false | _ -> true - + let IlAssemblyCodeHasEffect instrs = List.exists IlAssemblyCodeInstrHasEffect instrs +/// EI_ilzero embeds tyargs into IL; eliminating the binding is only safe when +/// the tyargs are fully ground (contain no free type variables, even after +/// following solutions). A binding whose type still references unsolved typars +/// is the only thing pinning those typars; removing it leaves them unsolved and +/// trips FS0073 in IlxGen. This is common for SRTP witness/dummy arguments. +let ILAsmWithIlzeroHasEffect instrs tyargs = + let hasIlzero = instrs |> List.exists (function EI_ilzero _ -> true | _ -> false) + + if not hasIlzero then + IlAssemblyCodeHasEffect instrs + else + let ilzeroIsSafe = + tyargs |> List.forall (fun ty -> Zset.isEmpty (freeInType CollectTypars ty).FreeTypars) + + let otherInstrsHaveEffect = + instrs |> List.exists (fun i -> match i with EI_ilzero _ -> false | _ -> IlAssemblyCodeInstrHasEffect i) + + otherInstrsHaveEffect || not ilzeroIsSafe + let rec ExprHasEffect g expr = match stripDebugPoints expr with | Expr.Val (vref, _, _) -> vref.IsTypeFunction || vref.IsMutable @@ -1661,7 +1681,7 @@ let rec ExprHasEffect g expr = | Expr.Const _ -> false // type applications do not have effects, with the exception of type functions | Expr.App (f0, _, _, [], _) -> IsTyFuncValRefExpr f0 || ExprHasEffect g f0 - | Expr.Op (op, _, args, m) -> ExprsHaveEffect g args || OpHasEffect g m op + | Expr.Op (op, tyargs, args, m) -> ExprsHaveEffect g args || OpHasEffect g m op tyargs | Expr.LetRec (binds, body, _, _) -> BindingsHaveEffect g binds || ExprHasEffect g body | Expr.Let (bind, body, _, _) -> BindingHasEffect g bind || ExprHasEffect g body // REVIEW: could add Expr.Obj on an interface type - these are similar to records of lambda expressions @@ -1673,7 +1693,7 @@ and BindingsHaveEffect g binds = List.exists (BindingHasEffect g) binds and BindingHasEffect g bind = bind.Expr |> ExprHasEffect g -and OpHasEffect g m op = +and OpHasEffect g m op tyargs = match op with | TOp.Tuple _ -> false | TOp.AnonRecd _ -> false @@ -1687,7 +1707,7 @@ and OpHasEffect g m op = | TOp.UnionCaseTagGet _ -> false | TOp.UnionCaseProof _ -> false | TOp.UnionCaseFieldGet (ucref, n) -> isUnionCaseFieldMutable g ucref n - | TOp.ILAsm (instrs, _) -> IlAssemblyCodeHasEffect instrs + | TOp.ILAsm (instrs, _) -> ILAsmWithIlzeroHasEffect instrs tyargs | TOp.TupleFieldGet _ -> false | TOp.ExnFieldGet (ecref, n) -> isExnFieldMutable ecref n | TOp.RefAddrGet _ -> false @@ -2618,7 +2638,7 @@ and OptimizeExprOp cenv env (op, tyargs, args, m) = newExpr, { TotalSize = 1 FunctionSize = 1 - HasEffect = OpHasEffect g m newOp + HasEffect = OpHasEffect g m newOp tyargs MightMakeCriticalTailcall = false Info = ValueOfExpr newExpr } @@ -2695,7 +2715,7 @@ and OptimizeExprOpFallback cenv env (op, tyargs, argsR, m) arginfos value_ = let argsFSize = AddFunctionSizes arginfos let argEffects = OrEffects arginfos let argValues = List.map (fun x -> x.Info) arginfos - let effect = OpHasEffect g m op + let effect = OpHasEffect g m op tyargs let cost, value_ = match op with | TOp.UnionCase c -> 2, MakeValueInfoForUnionCase c (Array.ofList argValues) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs index 4eb3dc57b98..90981fbee1b 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs @@ -218,3 +218,94 @@ let empty<'T> = Seq.empty<'T> .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 09 00 00 00 00 00 ) """ ] + + // https://github.com/dotnet/fsharp/issues/18128 + // Concrete-type Unchecked.defaultof bindings should be eliminated under optimization. + // Pins both the absence of initobj and that no decimal local slot is allocated for + // any of the three discarded bindings. + [] + let ``Issue_18128_Unchecked_defaultof_concrete_eliminated`` () = + FSharp """ +module Test + +open System + +let f (n: float32) = + Console.WriteLine n + let _ = Unchecked.defaultof + let _ = Unchecked.defaultof + let _ = Unchecked.defaultof + let n' = n * 2.f + Console.WriteLine n' +""" + |> asLibrary + |> withOptimize + |> compile + |> shouldSucceed + |> verifyILNotPresent [ "initobj"; "valuetype [runtime]System.Decimal" ] + |> ignore + + // https://github.com/dotnet/fsharp/issues/18128 + // The real-world FSharpPlus-style SRTP witness pattern from the issue. After elimination, + // doWork reduces to a direct double-precision multiplication; the nil and nil<^b> + // witness bindings are gone. + [] + let ``Issue_18128_SRTP_witness_pattern_compiles_and_optimizes`` () = + FSharp """ +module Test + +open System.ComponentModel +open FSharp.Core.LanguagePrimitives + +[] +type PreOps = + static member inline Double (n: float<'u>) : float<'u> = n * 2. + static member inline Double (n: float32<'u>) : float32<'u> = n * 2.f + +#nowarn "64" +module PreludeOperators = + let inline private nil<'T> = Unchecked.defaultof<'T> + let inline double (x: ^a) = + let inline _call (_: ^M, input: ^I, _: ^R) = ((^M or ^I) : (static member Double : ^I -> ^R) input) + _call (nil, x, nil< ^b >) + +open PreludeOperators +let doWork (n: float) = double n +""" + |> asLibrary + |> withOptimize + |> compile + |> shouldSucceed + |> verifyILNotPresent [ "initobj"; "ldnull" ] + |> ignore + + // Soundness pin: optimizing away an unused `Unchecked.defaultof` must not introduce + // a new reference to T in the enclosing method. `defaultof` of a reference type lowers + // to `ldnull` (not `newobj`), so the binding's removal cannot suppress an observable + // cctor call - f's body should contain no reference to WithCctor at all. + [] + let ``Issue_18128_eliminated_defaultof_leaves_no_reference_to_T_in_caller`` () = + FSharp """ +module Test + +type WithCctor() = + static do failwith "cctor must not run" + +let f () = + let _ = Unchecked.defaultof + 42 +""" + |> asLibrary + |> withOptimize + |> compile + |> shouldSucceed + |> verifyIL [ + """.method public static int32 f() cil managed + { + + .maxstack 8 + IL_0000: ldc.i4.s 42 + IL_0002: ret + }""" + ] + |> ignore diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs new file mode 100644 index 00000000000..b1ec7f8a017 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace EmittedIL + +open Xunit +open FSharp.Test.Compiler + +module ``UncheckedDefaultofOptimization`` = + + [] + let ``Unused Unchecked.defaultof bindings of concrete types are eliminated`` () = + FSharp """ +module Test +open System +let f (n: float32) = + Console.WriteLine n + let _ = Unchecked.defaultof + let _ = Unchecked.defaultof + let _ = Unchecked.defaultof + let n' = n * 2.f + Console.WriteLine n' + """ + |> withOptimize + |> asLibrary + |> compile + |> shouldSucceed + |> verifyILNotPresent ["initobj [runtime]System.Decimal"] + + [] + let ``Unused Unchecked.defaultof bindings of unsolved generic types do not cause FS0073`` () = + // Regression for SRTP witness/dummy-argument patterns (e.g. FSharpPlus) where + // eliminating an ilzero binding referencing unsolved typars trips FS0073 in IlxGen. + FSharp """ +module Test +let inline witness () = Unchecked.defaultof<'T> +let inline run< ^T when ^T: (static member Zero: ^T)> () = + let _ = (witness () : ^T) + LanguagePrimitives.GenericZero< ^T> +let v : int = run< int> () + """ + |> withOptimize + |> asLibrary + |> compile + |> shouldSucceed diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 2f96624917e..d626972831d 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -250,6 +250,7 @@ + From 8967ecdc68c914d8d1df06af606c4be432388f3e Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 30 Jun 2026 08:54:37 +0200 Subject: [PATCH 14/16] Consolidate defaultof tests; keep ilzero effect guard sound - Restore CodeGenRegressions_Observations.fs to main (it is a pre-existing file; the SRTP defaultof tests now live in the focused UncheckedDefaultofOptimization.fs). - UncheckedDefaultofOptimization.fs: 4 focused tests (concrete elimination, SRTP witness elimination, cctor soundness, FS0073 guard for unsolved typars). - Optimizer.fs: keep IlAssemblyCodeInstrHasEffect conservative for EI_ilzero; the only effect-free path for ilzero is the fully-ground-tyargs guard in ILAsmWithIlzeroHasEffect. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Optimize/Optimizer.fs | 3 +- .../CodeGenRegressions_Observations.fs | 91 ------------------- .../UncheckedDefaultofOptimization.fs | 74 ++++++++++++++- 3 files changed, 72 insertions(+), 96 deletions(-) diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index f8efa30dafa..e79e8601c91 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1647,8 +1647,7 @@ let IlAssemblyCodeInstrHasEffect i = | AI_nop | AI_ldc _ | AI_add | AI_sub | AI_mul | AI_xor | AI_and | AI_or | AI_ceq | AI_cgt | AI_cgt_un | AI_clt | AI_clt_un | AI_conv _ | AI_shl | AI_shr | AI_shr_un | AI_neg | AI_not | AI_ldnull - | I_ldstr _ | I_ldtoken _ - | EI_ilzero _ -> false + | I_ldstr _ | I_ldtoken _ -> false | _ -> true let IlAssemblyCodeHasEffect instrs = List.exists IlAssemblyCodeInstrHasEffect instrs diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs index 90981fbee1b..4eb3dc57b98 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/CodeGenRegressions/CodeGenRegressions_Observations.fs @@ -218,94 +218,3 @@ let empty<'T> = Seq.empty<'T> .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 09 00 00 00 00 00 ) """ ] - - // https://github.com/dotnet/fsharp/issues/18128 - // Concrete-type Unchecked.defaultof bindings should be eliminated under optimization. - // Pins both the absence of initobj and that no decimal local slot is allocated for - // any of the three discarded bindings. - [] - let ``Issue_18128_Unchecked_defaultof_concrete_eliminated`` () = - FSharp """ -module Test - -open System - -let f (n: float32) = - Console.WriteLine n - let _ = Unchecked.defaultof - let _ = Unchecked.defaultof - let _ = Unchecked.defaultof - let n' = n * 2.f - Console.WriteLine n' -""" - |> asLibrary - |> withOptimize - |> compile - |> shouldSucceed - |> verifyILNotPresent [ "initobj"; "valuetype [runtime]System.Decimal" ] - |> ignore - - // https://github.com/dotnet/fsharp/issues/18128 - // The real-world FSharpPlus-style SRTP witness pattern from the issue. After elimination, - // doWork reduces to a direct double-precision multiplication; the nil and nil<^b> - // witness bindings are gone. - [] - let ``Issue_18128_SRTP_witness_pattern_compiles_and_optimizes`` () = - FSharp """ -module Test - -open System.ComponentModel -open FSharp.Core.LanguagePrimitives - -[] -type PreOps = - static member inline Double (n: float<'u>) : float<'u> = n * 2. - static member inline Double (n: float32<'u>) : float32<'u> = n * 2.f - -#nowarn "64" -module PreludeOperators = - let inline private nil<'T> = Unchecked.defaultof<'T> - let inline double (x: ^a) = - let inline _call (_: ^M, input: ^I, _: ^R) = ((^M or ^I) : (static member Double : ^I -> ^R) input) - _call (nil, x, nil< ^b >) - -open PreludeOperators -let doWork (n: float) = double n -""" - |> asLibrary - |> withOptimize - |> compile - |> shouldSucceed - |> verifyILNotPresent [ "initobj"; "ldnull" ] - |> ignore - - // Soundness pin: optimizing away an unused `Unchecked.defaultof` must not introduce - // a new reference to T in the enclosing method. `defaultof` of a reference type lowers - // to `ldnull` (not `newobj`), so the binding's removal cannot suppress an observable - // cctor call - f's body should contain no reference to WithCctor at all. - [] - let ``Issue_18128_eliminated_defaultof_leaves_no_reference_to_T_in_caller`` () = - FSharp """ -module Test - -type WithCctor() = - static do failwith "cctor must not run" - -let f () = - let _ = Unchecked.defaultof - 42 -""" - |> asLibrary - |> withOptimize - |> compile - |> shouldSucceed - |> verifyIL [ - """.method public static int32 f() cil managed - { - - .maxstack 8 - IL_0000: ldc.i4.s 42 - IL_0002: ret - }""" - ] - |> ignore diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs index b1ec7f8a017..2dd26280128 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs @@ -7,6 +7,10 @@ open FSharp.Test.Compiler module ``UncheckedDefaultofOptimization`` = + // https://github.com/dotnet/fsharp/issues/18128 + // Unused `Unchecked.defaultof` bindings should be eliminated under optimization. + // Pins both the absence of `initobj` and that no decimal local slot is allocated for any of the + // three discarded bindings. [] let ``Unused Unchecked.defaultof bindings of concrete types are eliminated`` () = FSharp """ @@ -24,12 +28,76 @@ let f (n: float32) = |> asLibrary |> compile |> shouldSucceed - |> verifyILNotPresent ["initobj [runtime]System.Decimal"] + |> verifyILNotPresent [ "initobj"; "valuetype [runtime]System.Decimal" ] + // https://github.com/dotnet/fsharp/issues/18128 + // The FSharpPlus-style SRTP witness pattern from the issue. After elimination, `doWork` reduces to + // a direct multiplication; the `nil` and `nil< ^b >` witness bindings are gone. + [] + let ``Unused Unchecked.defaultof SRTP witness bindings are eliminated`` () = + FSharp """ +module Test + +open System.ComponentModel + +[] +type PreOps = + static member inline Double (n: float<'u>) : float<'u> = n * 2. + static member inline Double (n: float32<'u>) : float32<'u> = n * 2.f + +#nowarn "64" +module PreludeOperators = + let inline private nil<'T> = Unchecked.defaultof<'T> + let inline double (x: ^a) = + let inline _call (_: ^M, input: ^I, _: ^R) = ((^M or ^I) : (static member Double : ^I -> ^R) input) + _call (nil, x, nil< ^b >) + +open PreludeOperators +let doWork (n: float) = double n + """ + |> withOptimize + |> asLibrary + |> compile + |> shouldSucceed + |> verifyILNotPresent [ "initobj"; "ldnull" ] + + // https://github.com/dotnet/fsharp/issues/18128 + // Soundness pin: eliminating an unused `Unchecked.defaultof` must not introduce a new reference + // to T in the enclosing method. `defaultof` of a reference type lowers to `ldnull` (not `newobj`), + // so removing the binding cannot suppress an observable cctor call - `f`'s body must contain no + // reference to `WithCctor` at all. + [] + let ``Eliminated Unchecked.defaultof leaves no reference to T in the caller`` () = + FSharp """ +module Test + +type WithCctor() = + static do failwith "cctor must not run" + +let f () = + let _ = Unchecked.defaultof + 42 + """ + |> withOptimize + |> asLibrary + |> compile + |> shouldSucceed + |> verifyIL [ + """.method public static int32 f() cil managed + { + + .maxstack 8 + IL_0000: ldc.i4.s 42 + IL_0002: ret + }""" + ] + + // https://github.com/dotnet/fsharp/issues/18128 + // Regression for SRTP witness/dummy-argument patterns (e.g. FSharpPlus) where eliminating an + // `Unchecked.defaultof` binding whose type still references unsolved typars would trip FS0073 in + // IlxGen. Such bindings must be kept; only fully-ground ilzero bindings are removed. [] let ``Unused Unchecked.defaultof bindings of unsolved generic types do not cause FS0073`` () = - // Regression for SRTP witness/dummy-argument patterns (e.g. FSharpPlus) where - // eliminating an ilzero binding referencing unsolved typars trips FS0073 in IlxGen. FSharp """ module Test let inline witness () = Unchecked.defaultof<'T> From ae4869c37f7b0aa4bd5b9c0b4ff98b04169726fb Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 2 Jul 2026 10:04:30 +0200 Subject: [PATCH 15/16] Optimizer: preserve ilzero (Unchecked.defaultof) witness bindings in inline bodies Fixes the FSharpPlus NET10 regression (FS0073 'Undefined or unsolved type variable') introduced by eliminating unused Unchecked.defaultof<'T> bindings. Root cause: the elimination was applied while optimizing inline value bodies, whose optimized form is pickled as cross-assembly optimization info and re-inlined at each use site. Dropping an ilzero SRTP witness/dummy binding there corrupts that info; a consumer's IlxGen then finds a closure free type variable that is no longer pinned and reports FS0073. Elimination is safe at the fully-instantiated use site itself, so no codegen quality is lost. Fix: gate ilzero elimination on cenv.optimizing (false inside inline bodies). Only eliminate when optimizing for emission AND the erased tyargs are fully ground. Threads the flag through the effect-analysis helpers; the public ExprHasEffect entry point keeps emission-time semantics. Updates the SRTP-witness test to assert elimination at the use site (doWork), since an inline function's compiler-generated dynamic-invocation stub legitimately still contains ldnull. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/11.0.100.md | 2 +- src/Compiler/Optimize/Optimizer.fs | 49 ++++++++++++------- .../UncheckedDefaultofOptimization.fs | 22 +++++++-- 3 files changed, 49 insertions(+), 24 deletions(-) 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 c98b36b1d5e..a71ddc08c86 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -2,7 +2,7 @@ * Recursive `inline` functions and members now emit a single clear error (FS3890) instead of a misleading FS1113/FS1114 optimizer cascade. ([Issue #17991](https://github.com/dotnet/fsharp/issues/17991), [PR #19803](https://github.com/dotnet/fsharp/pull/19803)) * Report `FS0037 Duplicate definition of type or module` at type-check time when two sibling modules in a `module rec` / `namespace rec` group share a name, instead of letting the duplicate slip through to IL emit where it surfaced as the cryptic `FS2014: duplicate entry . in type index table`. ([Issue #6694](https://github.com/dotnet/fsharp/issues/6694), [PR #19913](https://github.com/dotnet/fsharp/pull/19913)) -* Unused `Unchecked.defaultof<'T>` bindings are now eliminated under optimization. This removes redundant `initobj`/`ldnull;pop` sequences emitted by SRTP helpers that use `Unchecked.defaultof` as dummy arguments to drive static-member-constraint resolution. ([Issue #18128](https://github.com/dotnet/fsharp/issues/18128), [PR #19758](https://github.com/dotnet/fsharp/pull/19758)) +* Unused `Unchecked.defaultof<'T>` bindings are now eliminated under optimization at their use sites. This removes redundant `initobj`/`ldnull;pop` sequences left behind after inlining SRTP helpers that use `Unchecked.defaultof` as dummy arguments to drive static-member-constraint resolution. Such bindings are preserved inside `inline` bodies (whose optimized form is pickled as cross-assembly optimization info and re-inlined by consumers) and whenever the erased type still references unsolved type variables, so no `FS0073` regression is introduced. ([Issue #18128](https://github.com/dotnet/fsharp/issues/18128), [PR #19758](https://github.com/dotnet/fsharp/pull/19758)) * Semantic classification no longer marks recursive object self-references (`as this`, `let rec` self-refs) as mutable. ([Issue #5229](https://github.com/dotnet/fsharp/issues/5229)) * Fix `MethodAccessException` under `--realsig+` when a closure (inner `let rec`, `task`/`async` state machine, or quotation splice) inside a member defined in an intrinsic type augmentation (`type C with member ...`) accesses a `private` member of `C`. The synthesized closure is now nested inside the declaring type instead of beside it in the module class. ([Issue #19933](https://github.com/dotnet/fsharp/issues/19933), [PR #19955](https://github.com/dotnet/fsharp/pull/19955)) * Preserve source range for type errors on empty-bodied computation expressions (e.g. `foo {}`) in pipelines, function arguments, and type-annotated contexts, instead of reporting `unknown(1,1)`. ([Issue #19550](https://github.com/dotnet/fsharp/issues/19550), [PR #19849](https://github.com/dotnet/fsharp/pull/19849)) diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index e79e8601c91..a5490cf8e66 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1652,18 +1652,24 @@ let IlAssemblyCodeInstrHasEffect i = let IlAssemblyCodeHasEffect instrs = List.exists IlAssemblyCodeInstrHasEffect instrs -/// EI_ilzero embeds tyargs into IL; eliminating the binding is only safe when -/// the tyargs are fully ground (contain no free type variables, even after -/// following solutions). A binding whose type still references unsolved typars -/// is the only thing pinning those typars; removing it leaves them unsolved and -/// trips FS0073 in IlxGen. This is common for SRTP witness/dummy arguments. -let ILAsmWithIlzeroHasEffect instrs tyargs = +/// EI_ilzero embeds tyargs into IL; eliminating the binding is only safe when: +/// - We are optimizing for emission (optimizing = true). Inside an inline value's body +/// (optimizing = false) the optimized expression is pickled as cross-assembly optimization +/// info and re-inlined at each use site; dropping an ilzero witness/dummy binding there +/// corrupts that info and trips FS0073 in a consumer's IlxGen (e.g. the SRTP witness/dummy +/// arguments used pervasively by FSharpPlus). Elimination is still performed at the (fully +/// instantiated) use site itself, where optimizing = true, so no codegen quality is lost. +/// - The tyargs are fully ground (contain no free type variables, even after following +/// solutions). A binding whose type still references unsolved typars is the only thing +/// pinning those typars; removing it leaves them unsolved and also trips FS0073. +let ILAsmWithIlzeroHasEffect optimizing instrs tyargs = let hasIlzero = instrs |> List.exists (function EI_ilzero _ -> true | _ -> false) if not hasIlzero then IlAssemblyCodeHasEffect instrs else let ilzeroIsSafe = + optimizing && tyargs |> List.forall (fun ty -> Zset.isEmpty (freeInType CollectTypars ty).FreeTypars) let otherInstrsHaveEffect = @@ -1671,7 +1677,7 @@ let ILAsmWithIlzeroHasEffect instrs tyargs = otherInstrsHaveEffect || not ilzeroIsSafe -let rec ExprHasEffect g expr = +let rec ExprHasEffectImpl optimizing g expr = match stripDebugPoints expr with | Expr.Val (vref, _, _) -> vref.IsTypeFunction || vref.IsMutable | Expr.Quote _ @@ -1679,20 +1685,20 @@ let rec ExprHasEffect g expr = | Expr.TyLambda _ | Expr.Const _ -> false // type applications do not have effects, with the exception of type functions - | Expr.App (f0, _, _, [], _) -> IsTyFuncValRefExpr f0 || ExprHasEffect g f0 - | Expr.Op (op, tyargs, args, m) -> ExprsHaveEffect g args || OpHasEffect g m op tyargs - | Expr.LetRec (binds, body, _, _) -> BindingsHaveEffect g binds || ExprHasEffect g body - | Expr.Let (bind, body, _, _) -> BindingHasEffect g bind || ExprHasEffect g body + | Expr.App (f0, _, _, [], _) -> IsTyFuncValRefExpr f0 || ExprHasEffectImpl optimizing g f0 + | Expr.Op (op, tyargs, args, m) -> ExprsHaveEffectImpl optimizing g args || OpHasEffectImpl optimizing g m op tyargs + | Expr.LetRec (binds, body, _, _) -> BindingsHaveEffectImpl optimizing g binds || ExprHasEffectImpl optimizing g body + | Expr.Let (bind, body, _, _) -> BindingHasEffectImpl optimizing g bind || ExprHasEffectImpl optimizing g body // REVIEW: could add Expr.Obj on an interface type - these are similar to records of lambda expressions | _ -> true -and ExprsHaveEffect g exprs = List.exists (ExprHasEffect g) exprs +and ExprsHaveEffectImpl optimizing g exprs = List.exists (ExprHasEffectImpl optimizing g) exprs -and BindingsHaveEffect g binds = List.exists (BindingHasEffect g) binds +and BindingsHaveEffectImpl optimizing g binds = List.exists (BindingHasEffectImpl optimizing g) binds -and BindingHasEffect g bind = bind.Expr |> ExprHasEffect g +and BindingHasEffectImpl optimizing g bind = bind.Expr |> ExprHasEffectImpl optimizing g -and OpHasEffect g m op tyargs = +and OpHasEffectImpl optimizing g m op tyargs = match op with | TOp.Tuple _ -> false | TOp.AnonRecd _ -> false @@ -1706,7 +1712,7 @@ and OpHasEffect g m op tyargs = | TOp.UnionCaseTagGet _ -> false | TOp.UnionCaseProof _ -> false | TOp.UnionCaseFieldGet (ucref, n) -> isUnionCaseFieldMutable g ucref n - | TOp.ILAsm (instrs, _) -> ILAsmWithIlzeroHasEffect instrs tyargs + | TOp.ILAsm (instrs, _) -> ILAsmWithIlzeroHasEffect optimizing instrs tyargs | TOp.TupleFieldGet _ -> false | TOp.ExnFieldGet (ecref, n) -> isExnFieldMutable ecref n | TOp.RefAddrGet _ -> false @@ -1733,6 +1739,11 @@ and OpHasEffect g m op tyargs = | TOp.LValueOp _ (* conservative *) | TOp.ValFieldSet _ -> true +/// Check if an expression has an effect. This is the entry point used outside the optimizer +/// (e.g. IlxGen), so it uses the emission-time semantics (optimizing = true) under which a +/// fully-ground `Unchecked.defaultof` (ilzero) is effect-free. +let ExprHasEffect g expr = ExprHasEffectImpl true g expr + let TryEliminateBinding cenv _env bind e2 _m = let g = cenv.g @@ -1762,7 +1773,7 @@ let TryEliminateBinding cenv _env bind e2 _m = match argsr with | Expr.Val (VRefLocal vspec2, _, _) :: argsr2 when valEq vspec1 vspec2 && IsUniqueUse vspec2 (List.rev rargsl@argsr2) -> Some(List.rev rargsl, argsr2) - | argsrh :: argsrt when not (ExprHasEffect g argsrh) -> GetImmediateUseContext (argsrh :: rargsl) argsrt + | argsrh :: argsrt when not (ExprHasEffectImpl cenv.optimizing g argsrh) -> GetImmediateUseContext (argsrh :: rargsl) argsrt | _ -> None let (DebugPoints(e2, recreate0)) = e2 @@ -2637,7 +2648,7 @@ and OptimizeExprOp cenv env (op, tyargs, args, m) = newExpr, { TotalSize = 1 FunctionSize = 1 - HasEffect = OpHasEffect g m newOp tyargs + HasEffect = OpHasEffectImpl cenv.optimizing g m newOp tyargs MightMakeCriticalTailcall = false Info = ValueOfExpr newExpr } @@ -2714,7 +2725,7 @@ and OptimizeExprOpFallback cenv env (op, tyargs, argsR, m) arginfos value_ = let argsFSize = AddFunctionSizes arginfos let argEffects = OrEffects arginfos let argValues = List.map (fun x -> x.Info) arginfos - let effect = OpHasEffect g m op tyargs + let effect = OpHasEffectImpl cenv.optimizing g m op tyargs let cost, value_ = match op with | TOp.UnionCase c -> 2, MakeValueInfoForUnionCase c (Array.ofList argValues) diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs index 2dd26280128..43688b29c5c 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/UncheckedDefaultofOptimization.fs @@ -31,10 +31,14 @@ let f (n: float32) = |> verifyILNotPresent [ "initobj"; "valuetype [runtime]System.Decimal" ] // https://github.com/dotnet/fsharp/issues/18128 - // The FSharpPlus-style SRTP witness pattern from the issue. After elimination, `doWork` reduces to - // a direct multiplication; the `nil` and `nil< ^b >` witness bindings are gone. + // The FSharpPlus-style SRTP witness pattern from the issue. Elimination happens at the (fully + // instantiated) use site: `doWork` reduces to a direct multiplication with the `nil` and + // `nil< ^b >` witness bindings gone. The assertion is scoped to `doWork` rather than the whole + // module because the inline functions also emit a compiler-generated dynamic-invocation stub which + // legitimately retains an `ldnull` (see the guard below and issue #19758 - the witness bindings of + // an *inline* body must be preserved so cross-assembly consumers can re-inline and specialize them). [] - let ``Unused Unchecked.defaultof SRTP witness bindings are eliminated`` () = + let ``Unused Unchecked.defaultof SRTP witness bindings are eliminated at the use site`` () = FSharp """ module Test @@ -59,7 +63,17 @@ let doWork (n: float) = double n |> asLibrary |> compile |> shouldSucceed - |> verifyILNotPresent [ "initobj"; "ldnull" ] + |> verifyIL [ + """.method public static float64 doWork(float64 n) cil managed + { + + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldc.r8 2. + IL_000a: mul + IL_000b: ret + }""" + ] // https://github.com/dotnet/fsharp/issues/18128 // Soundness pin: eliminating an unused `Unchecked.defaultof` must not introduce a new reference From 8d4f7c1d43aaa215733ed514c87eb10cac772dc8 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 2 Jul 2026 12:56:12 +0200 Subject: [PATCH 16/16] Optimizer: replace effect-check 'optimizing' bool with an EffectContext DU Drop the *Impl-suffix scaffolding on the effect-checking functions and replace the threaded 'optimizing: bool' with a self-documenting EffectContext = Emit | InlineBody DU. IlxGen passes EffectContext.Emit; internal call sites map the pre-existing cenv.optimizing field via a one-line helper. Behavior-preserving. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Compiler/CodeGen/IlxGen.fs | 2 +- src/Compiler/Optimize/Optimizer.fs | 71 +++++++++++++++-------------- src/Compiler/Optimize/Optimizer.fsi | 10 +++- 3 files changed, 48 insertions(+), 35 deletions(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index c7100cd69aa..72324db79a2 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -4706,7 +4706,7 @@ and GenApp (cenv: cenv) cgbuf eenv (f, fty, tyargs, curriedArgs, m) sequel = (eenv, laterArgs) ||> List.mapFold (fun eenv laterArg -> // Only save arguments that have effects - if Optimizer.ExprHasEffect g laterArg then + if Optimizer.ExprHasEffect Optimizer.EffectContext.Emit g laterArg then let ilTy = laterArg |> tyOfExpr g |> GenType cenv m eenv.tyenv let locName = diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index 2664cffa853..8f3a550bdb7 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -1624,32 +1624,39 @@ let IlAssemblyCodeInstrHasEffect i = let IlAssemblyCodeHasEffect instrs = List.exists IlAssemblyCodeInstrHasEffect instrs -/// EI_ilzero embeds tyargs into IL; eliminating the binding is only safe when: -/// - We are optimizing for emission (optimizing = true). Inside an inline value's body -/// (optimizing = false) the optimized expression is pickled as cross-assembly optimization -/// info and re-inlined at each use site; dropping an ilzero witness/dummy binding there -/// corrupts that info and trips FS0073 in a consumer's IlxGen (e.g. the SRTP witness/dummy -/// arguments used pervasively by FSharpPlus). Elimination is still performed at the (fully -/// instantiated) use site itself, where optimizing = true, so no codegen quality is lost. -/// - The tyargs are fully ground (contain no free type variables, even after following -/// solutions). A binding whose type still references unsolved typars is the only thing -/// pinning those typars; removing it leaves them unsolved and also trips FS0073. -let ILAsmWithIlzeroHasEffect optimizing instrs tyargs = +/// The context an expression's effects are analyzed in. It governs whether a fully-ground +/// `Unchecked.defaultof` (an `EI_ilzero` witness) may be treated as effect-free and its unused +/// binding dropped: safe when emitting here, but not inside an `inline` value's body, whose +/// optimized form is pickled as cross-assembly info and re-inlined at each use site — dropping +/// the witness there corrupts that info and trips FS0073 in the consumer's IlxGen (e.g. the SRTP +/// witness/dummy arguments used pervasively by FSharpPlus). +[] +type EffectContext = + /// Emitting IL here, at a fully instantiated use site. + | Emit + /// Analyzing the pickled body of an `inline` value, re-inlined at other sites. + | InlineBody + +let ILAsmWithIlzeroHasEffect context instrs tyargs = let hasIlzero = instrs |> List.exists (function EI_ilzero _ -> true | _ -> false) if not hasIlzero then IlAssemblyCodeHasEffect instrs else - let ilzeroIsSafe = - optimizing && - tyargs |> List.forall (fun ty -> Zset.isEmpty (freeInType CollectTypars ty).FreeTypars) + // A fully-ground ilzero is a pure default-value load whose unused binding may be dropped, + // but only at emission and only when no tyarg still holds a free typar: such a binding is + // the sole pin for those typars and dropping it leaves them unsolved (FS0073). + let ilzeroIsSafeToDrop = + match context with + | EffectContext.Emit -> tyargs |> List.forall (fun ty -> Zset.isEmpty (freeInType CollectTypars ty).FreeTypars) + | EffectContext.InlineBody -> false let otherInstrsHaveEffect = - instrs |> List.exists (fun i -> match i with EI_ilzero _ -> false | _ -> IlAssemblyCodeInstrHasEffect i) + instrs |> List.exists (function EI_ilzero _ -> false | i -> IlAssemblyCodeInstrHasEffect i) - otherInstrsHaveEffect || not ilzeroIsSafe + otherInstrsHaveEffect || not ilzeroIsSafeToDrop -let rec ExprHasEffectImpl optimizing g expr = +let rec ExprHasEffect context g expr = match stripDebugPoints expr with | Expr.Val (vref, _, _) -> vref.IsTypeFunction || vref.IsMutable | Expr.Quote _ @@ -1657,20 +1664,20 @@ let rec ExprHasEffectImpl optimizing g expr = | Expr.TyLambda _ | Expr.Const _ -> false // type applications do not have effects, with the exception of type functions - | Expr.App (f0, _, _, [], _) -> IsTyFuncValRefExpr f0 || ExprHasEffectImpl optimizing g f0 - | Expr.Op (op, tyargs, args, m) -> ExprsHaveEffectImpl optimizing g args || OpHasEffectImpl optimizing g m op tyargs - | Expr.LetRec (binds, body, _, _) -> BindingsHaveEffectImpl optimizing g binds || ExprHasEffectImpl optimizing g body - | Expr.Let (bind, body, _, _) -> BindingHasEffectImpl optimizing g bind || ExprHasEffectImpl optimizing g body + | Expr.App (f0, _, _, [], _) -> IsTyFuncValRefExpr f0 || ExprHasEffect context g f0 + | Expr.Op (op, tyargs, args, m) -> ExprsHaveEffect context g args || OpHasEffect context g m op tyargs + | Expr.LetRec (binds, body, _, _) -> BindingsHaveEffect context g binds || ExprHasEffect context g body + | Expr.Let (bind, body, _, _) -> BindingHasEffect context g bind || ExprHasEffect context g body // REVIEW: could add Expr.Obj on an interface type - these are similar to records of lambda expressions | _ -> true -and ExprsHaveEffectImpl optimizing g exprs = List.exists (ExprHasEffectImpl optimizing g) exprs +and ExprsHaveEffect context g exprs = List.exists (ExprHasEffect context g) exprs -and BindingsHaveEffectImpl optimizing g binds = List.exists (BindingHasEffectImpl optimizing g) binds +and BindingsHaveEffect context g binds = List.exists (BindingHasEffect context g) binds -and BindingHasEffectImpl optimizing g bind = bind.Expr |> ExprHasEffectImpl optimizing g +and BindingHasEffect context g bind = bind.Expr |> ExprHasEffect context g -and OpHasEffectImpl optimizing g m op tyargs = +and OpHasEffect context g m op tyargs = match op with | TOp.Tuple _ -> false | TOp.AnonRecd _ -> false @@ -1684,7 +1691,7 @@ and OpHasEffectImpl optimizing g m op tyargs = | TOp.UnionCaseTagGet _ -> false | TOp.UnionCaseProof _ -> false | TOp.UnionCaseFieldGet (ucref, n) -> isUnionCaseFieldMutable g ucref n - | TOp.ILAsm (instrs, _) -> ILAsmWithIlzeroHasEffect optimizing instrs tyargs + | TOp.ILAsm (instrs, _) -> ILAsmWithIlzeroHasEffect context instrs tyargs | TOp.TupleFieldGet _ -> false | TOp.ExnFieldGet (ecref, n) -> isExnFieldMutable ecref n | TOp.RefAddrGet _ -> false @@ -1711,10 +1718,8 @@ and OpHasEffectImpl optimizing g m op tyargs = | TOp.LValueOp _ (* conservative *) | TOp.ValFieldSet _ -> true -/// Check if an expression has an effect. This is the entry point used outside the optimizer -/// (e.g. IlxGen), so it uses the emission-time semantics (optimizing = true) under which a -/// fully-ground `Unchecked.defaultof` (ilzero) is effect-free. -let ExprHasEffect g expr = ExprHasEffectImpl true g expr +let effectContextOf (cenv: cenv) = + if cenv.optimizing then EffectContext.Emit else EffectContext.InlineBody let TryEliminateBinding cenv _env bind e2 _m = @@ -1745,7 +1750,7 @@ let TryEliminateBinding cenv _env bind e2 _m = match argsr with | Expr.Val (VRefLocal vspec2, _, _) :: argsr2 when valEq vspec1 vspec2 && IsUniqueUse vspec2 (List.rev rargsl@argsr2) -> Some(List.rev rargsl, argsr2) - | argsrh :: argsrt when not (ExprHasEffectImpl cenv.optimizing g argsrh) -> GetImmediateUseContext (argsrh :: rargsl) argsrt + | argsrh :: argsrt when not (ExprHasEffect (effectContextOf cenv) g argsrh) -> GetImmediateUseContext (argsrh :: rargsl) argsrt | _ -> None let (DebugPoints(e2, recreate0)) = e2 @@ -2620,7 +2625,7 @@ and OptimizeExprOp cenv env (op, tyargs, args, m) = newExpr, { TotalSize = 1 FunctionSize = 1 - HasEffect = OpHasEffectImpl cenv.optimizing g m newOp tyargs + HasEffect = OpHasEffect (effectContextOf cenv) g m newOp tyargs MightMakeCriticalTailcall = false Info = ValueOfExpr newExpr } @@ -2697,7 +2702,7 @@ and OptimizeExprOpFallback cenv env (op, tyargs, argsR, m) arginfos value_ = let argsFSize = AddFunctionSizes arginfos let argEffects = OrEffects arginfos let argValues = List.map (fun x -> x.Info) arginfos - let effect = OpHasEffectImpl cenv.optimizing g m op tyargs + let effect = OpHasEffect (effectContextOf cenv) g m op tyargs let cost, value_ = match op with | TOp.UnionCase c -> 2, MakeValueInfoForUnionCase c (Array.ofList argValues) diff --git a/src/Compiler/Optimize/Optimizer.fsi b/src/Compiler/Optimize/Optimizer.fsi index 17912af7598..8ec9b556ca2 100644 --- a/src/Compiler/Optimize/Optimizer.fsi +++ b/src/Compiler/Optimize/Optimizer.fsi @@ -108,8 +108,16 @@ val AbstractOptimizationInfoToEssentials: (CcuOptimizationInfo -> CcuOptimizatio /// Combine optimization infos val UnionOptimizationInfos: seq -> CcuOptimizationInfo +/// The context an expression's effects are analyzed in: emitting IL here, or analyzing the +/// pickled body of an `inline` value. Governs whether a fully-ground `Unchecked.defaultof` +/// binding may be eliminated. +[] +type EffectContext = + | Emit + | InlineBody + /// Check if an expression has an effect -val ExprHasEffect: TcGlobals -> Expr -> bool +val ExprHasEffect: EffectContext -> TcGlobals -> Expr -> bool val internal u_CcuOptimizationInfo: ReaderState -> CcuOptimizationInfo