From 7ad68b7a6841c0bbfce73a85a0435c03afd1d346 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Wed, 20 May 2026 08:45:53 +0200 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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