Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7ad68b7
Fix EI_ilzero optimization: treat Unchecked.defaultof as effect-free …
T-Gro May 20, 2026
8e66338
Use freeInTypes to detect nested type variables in EI_ilzero check
T-Gro May 20, 2026
ccefa82
Use freeInTypes to detect nested type variables in EI_ilzero check
T-Gro May 20, 2026
79d9895
Refactor EI_ilzero effect check: narrow to ILAsm, add non-regression …
T-Gro May 29, 2026
d68c69b
Strip dead-code FS0073 protection; restore release notes; fix issue refs
T-Gro Jun 1, 2026
72b85a3
Round 2 polish: stronger IL assertions, cctor-soundness test, drop ja…
T-Gro Jun 1, 2026
2c7b57f
Round 3 polish: drop stray blank line, rename cctor test for accuracy
T-Gro Jun 1, 2026
ec7e2ac
Merge branch 'main' into copilot/fix-unchecked-defaultof-optimization
T-Gro Jun 1, 2026
f293050
Restore tyargsContainFreeTypars guard for EI_ilzero; fix IL test format
Jun 3, 2026
e0353b6
Merge remote-tracking branch 'origin/main' into copilot/fix-unchecked…
Jun 3, 2026
8708a42
Simplify ILAsmWithIlzeroHasEffect to also check other instrs in list
Jun 3, 2026
f2c735d
Merge branch 'main' into copilot/fix-unchecked-defaultof-optimization
T-Gro Jun 3, 2026
7d8b06d
Fix EI_ilzero effect check: require concrete TType_app (not TType_var)
Jun 3, 2026
57de6b2
Merge branch 'main' into copilot/fix-unchecked-defaultof-optimization
T-Gro Jun 18, 2026
4a9364e
Merge branch 'main' into copilot/fix-unchecked-defaultof-optimization
T-Gro Jun 23, 2026
08eb0f4
Merge remote-tracking branch 'origin/main' into copilot/fix-unchecked…
Jun 29, 2026
44d01eb
Refine ilzero effect-freedom to fully-ground types; add tests
Jun 29, 2026
1bcc1c2
Revert defaultof elimination: unsafe for SRTP witness typars
Jun 29, 2026
87e2130
Merge remote-tracking branch 'origin/main' into copilot/fix-unchecked…
Jun 30, 2026
12dadbf
Revert "Revert defaultof elimination: unsafe for SRTP witness typars"
Jun 30, 2026
8967ecd
Consolidate defaultof tests; keep ilzero effect guard sound
Jun 30, 2026
ae4869c
Optimizer: preserve ilzero (Unchecked.defaultof) witness bindings in …
Jul 2, 2026
d5da31a
Merge remote-tracking branch 'origin/main' into copilot/fix-unchecked…
Jul 2, 2026
8d4f7c1
Optimizer: replace effect-check 'optimizing' bool with an EffectConte…
T-Gro Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* Diagnostic FS0027 now emits a parameter-specific message (suggesting a `let mutable x = x` shadow or `byref<_>`) instead of the illegal `let mutable x = expression` shadow when the assignment target is a function or method parameter. ([Issue #15803](https://github.com/dotnet/fsharp/issues/15803), [PR #19866](https://github.com/dotnet/fsharp/pull/19866))
* 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 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))
Expand Down
2 changes: 1 addition & 1 deletion src/Compiler/CodeGen/IlxGen.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
69 changes: 52 additions & 17 deletions src/Compiler/Optimize/Optimizer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1616,36 +1616,68 @@ 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 _ -> false
| _ -> true

let IlAssemblyCodeHasEffect instrs = List.exists IlAssemblyCodeInstrHasEffect instrs

let rec ExprHasEffect g expr =
/// 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).
[<RequireQualifiedAccess>]
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
// 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 (function EI_ilzero _ -> false | i -> IlAssemblyCodeInstrHasEffect i)

otherInstrsHaveEffect || not ilzeroIsSafeToDrop

let rec ExprHasEffect context g expr =
match stripDebugPoints expr with
| Expr.Val (vref, _, _) -> vref.IsTypeFunction || vref.IsMutable
| Expr.Quote _
| Expr.Lambda _
| 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, _, 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
| 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 ExprsHaveEffect g exprs = List.exists (ExprHasEffect g) exprs
and ExprsHaveEffect context g exprs = List.exists (ExprHasEffect context g) exprs

and BindingsHaveEffect g binds = List.exists (BindingHasEffect g) binds
and BindingsHaveEffect context g binds = List.exists (BindingHasEffect context g) binds

and BindingHasEffect g bind = bind.Expr |> ExprHasEffect g
and BindingHasEffect context g bind = bind.Expr |> ExprHasEffect context g

and OpHasEffect g m op =
and OpHasEffect context g m op tyargs =
match op with
| TOp.Tuple _ -> false
| TOp.AnonRecd _ -> false
Expand All @@ -1659,7 +1691,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 context instrs tyargs
| TOp.TupleFieldGet _ -> false
| TOp.ExnFieldGet (ecref, n) -> isExnFieldMutable ecref n
| TOp.RefAddrGet _ -> false
Expand All @@ -1686,6 +1718,9 @@ and OpHasEffect g m op =
| TOp.LValueOp _ (* conservative *)
| TOp.ValFieldSet _ -> true

let effectContextOf (cenv: cenv) =
if cenv.optimizing then EffectContext.Emit else EffectContext.InlineBody


let TryEliminateBinding cenv _env bind e2 _m =
let g = cenv.g
Expand Down Expand Up @@ -1715,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 (ExprHasEffect 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
Expand Down Expand Up @@ -2590,7 +2625,7 @@ and OptimizeExprOp cenv env (op, tyargs, args, m) =
newExpr,
{ TotalSize = 1
FunctionSize = 1
HasEffect = OpHasEffect g m newOp
HasEffect = OpHasEffect (effectContextOf cenv) g m newOp tyargs
MightMakeCriticalTailcall = false
Info = ValueOfExpr newExpr }

Expand Down Expand Up @@ -2667,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 = OpHasEffect g m op
let effect = OpHasEffect (effectContextOf cenv) g m op tyargs
let cost, value_ =
match op with
| TOp.UnionCase c -> 2, MakeValueInfoForUnionCase c (Array.ofList argValues)
Expand Down
10 changes: 9 additions & 1 deletion src/Compiler/Optimize/Optimizer.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,16 @@ val AbstractOptimizationInfoToEssentials: (CcuOptimizationInfo -> CcuOptimizatio
/// Combine optimization infos
val UnionOptimizationInfos: seq<ImplFileOptimizationInfo> -> 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.
[<RequireQualifiedAccess>]
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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// 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`` =

// https://github.com/dotnet/fsharp/issues/18128
// Unused `Unchecked.defaultof<concreteType>` 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.
[<Fact>]
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<decimal>
let _ = Unchecked.defaultof<decimal>
let _ = Unchecked.defaultof<decimal>
let n' = n * 2.f
Console.WriteLine n'
"""
|> withOptimize
|> asLibrary
|> compile
|> shouldSucceed
|> verifyILNotPresent [ "initobj"; "valuetype [runtime]System.Decimal" ]

// https://github.com/dotnet/fsharp/issues/18128
// 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<PreOps>` 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).
[<Fact>]
let ``Unused Unchecked.defaultof SRTP witness bindings are eliminated at the use site`` () =
FSharp """
module Test
open System.ComponentModel
[<AbstractClass; Sealed; EditorBrowsable(EditorBrowsableState.Never)>]
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<PreOps>, x, nil< ^b >)
open PreludeOperators
let doWork (n: float) = double n
"""
|> withOptimize
|> asLibrary
|> 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
}"""
]

// https://github.com/dotnet/fsharp/issues/18128
// Soundness pin: eliminating an unused `Unchecked.defaultof<T>` 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.
[<Fact>]
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<WithCctor>
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.
[<Fact>]
let ``Unused Unchecked.defaultof bindings of unsolved generic types do not cause FS0073`` () =
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
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@
<!--<Compile Include="EmittedIL\StructGettersReadOnly.fs" />-->
<Compile Include="EmittedIL\TailCalls.fs" />
<Compile Include="EmittedIL\TupleElimination.fs" />
<Compile Include="EmittedIL\UncheckedDefaultofOptimization.fs" />
<Compile Include="EmittedIL\TypeTestsInPatternMatching.fs" />
<Compile Include="EmittedIL\WhileLoops.fs" />
<Compile Include="EmittedIL\ArgumentNames.fs" />
Expand Down
Loading