Skip to content

Optimizer: treat EI_ilzero (Unchecked.defaultof) as effect-free to enable dead binding elimination#19758

Open
Copilot wants to merge 3 commits into
mainfrom
copilot/fix-unchecked-defaultof-optimization
Open

Optimizer: treat EI_ilzero (Unchecked.defaultof) as effect-free to enable dead binding elimination#19758
Copilot wants to merge 3 commits into
mainfrom
copilot/fix-unchecked-defaultof-optimization

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 18, 2026

Unchecked.defaultof<T> unused bindings were not eliminated even with optimizations enabled, because the optimizer's effect analysis incorrectly classified the underlying EI_ilzero IL instruction as having side effects.

Root cause

Unchecked.defaultof<T> inlines to (# "ilzero !0" type ('T) : 'T #), which the optimizer represents as TOp.ILAsm([EI_ilzero ty], ...). The IlAssemblyCodeInstrHasEffect function in Optimizer.fs did not recognize EI_ilzero as effect-free, so it fell through to the conservative _ -> true default, preventing dead binding elimination.

Change

src/Compiler/Optimize/Optimizer.fs — add EI_ilzero _ to the no-effect branch of IlAssemblyCodeInstrHasEffect. The instruction only produces a zero/default value (mapping to ldnull, ldc.i4.0, or initobj depending on type), with no observable side effects.

// Before: initobj instructions emitted for all three unused bindings
let f (n: float32) =
    Console.WriteLine n
    let _ = Unchecked.defaultof<decimal>   // initobj Decimal
    let _ = Unchecked.defaultof<decimal>   // initobj Decimal
    let _ = Unchecked.defaultof<decimal>   // initobj Decimal
    let n' = n * 2.f
    Console.WriteLine n'

// After: all three bindings eliminated; IL only contains the two Console.WriteLine calls

This is particularly relevant for SRTP-based patterns (common in libraries like FSharpPlus) where Unchecked.defaultof<'T> is used as a witness/dummy argument to drive overload resolution and ends up as unused bindings at call sites.

Copilot AI linked an issue May 18, 2026 that may be closed by this pull request
Copilot AI changed the title [WIP] Fix unchecked.defaultof preventing other optimizations Optimizer: treat EI_ilzero (Unchecked.defaultof) as effect-free to enable dead binding elimination May 18, 2026
Copilot finished work on behalf of T-Gro May 18, 2026 11:41
Copilot AI requested a review from T-Gro May 18, 2026 11:41
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

❗ Release notes required


✅ Found changes and release notes in following paths:

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/11.0.100.md

…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<decimal>`.

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>
@T-Gro T-Gro force-pushed the copilot/fix-unchecked-defaultof-optimization branch from 8b99fdc to 7ad68b7 Compare May 20, 2026 06:46
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>
@T-Gro T-Gro marked this pull request as ready for review May 20, 2026 08:32
@T-Gro T-Gro requested a review from a team as a code owner May 20, 2026 08:32
@github-actions github-actions Bot added the AI-Tooling-Check-Bypassed Tooling check: non-fork PR, not diff-analyzed label May 20, 2026
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>
@T-Gro T-Gro force-pushed the copilot/fix-unchecked-defaultof-optimization branch from 17e17e9 to ccefa82 Compare May 20, 2026 10:10
Copy link
Copy Markdown
Member

@T-Gro T-Gro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Optimizer – treat EI_ilzero as effect-free

Overall: Solid fix. The root-cause analysis is correct, the code change is minimal, and both positive and negative test cases are included. Approve direction; a few observations below.


1. Breadth of the ExprHasEffect / OptimizeExprOpFallback type-var guard

The freeInTypes check is now applied to all Expr.Op nodes that are otherwise effect-free, not only TOp.ILAsm [EI_ilzero _].
This means dead-binding elimination is also blocked for unused tuples, union cases, struct records, etc. when their tyargs reference a free typar – even the function's own type parameter (which is safe to reference via the IL generic context).

Example that could be affected:

sharp let f<'T> () = let _ = Unchecked.defaultof<'T> // safe to eliminate, 'T is the function's own typar 42

In practice this is unlikely to matter (who writes unused defaultof with their own typar?), but it would be worth adding a comment explaining the trade-off: "we accept false positives for non-SRTP typars to keep the fix simple."

If you later want a tighter check, you could limit the guard to TOp.ILAsm containing EI_ilzero, since that's the only instruction that can appear with zero value-args in an expression whose sole purpose is providing an SRTP witness/dummy.

2. Duplicate logic – consider a helper

The same freeInTypes CollectTyparsNoCaching tyargs check appears verbatim at lines 1642 and 2663. A small helper like:

sharp let tyargsHaveFreeTypars tyargs = not (List.isEmpty tyargs) && not (Zset.isEmpty (freeInTypes CollectTyparsNoCaching tyargs).FreeTypars)

would reduce duplication and make intent clearer at both call sites.

3. Performance consideration

freeInTypes traverses the type list and allocates a FreeTyvars. It's now called on every Expr.Op with non-empty tyargs whose op+args are effect-free. In large optimized builds this could add up. Have you spot-checked compiler self-build time or BenchmarkDotNet traces to confirm it's negligible? The "NoCaching" variant means no memoization across calls.

4. Tests

Good coverage:

  • Concrete type → eliminated ✓
  • SRTP type var (including nested Wrapper<^T>) → preserved ✓

Optional addition: a test with a non-SRTP generic function demonstrating the conservative "keep" behavior (to document expected behavior and guard against future over-optimization):

sharp let f<'T> () = let _ = Unchecked.defaultof<'T> 42

5. Minor: release-notes wording

"for concrete types"

The optimization applies whenever tyargs have no free typars, so Unchecked.defaultof<list<int>> is also eliminated – "concrete" is correct but might be clearer as "fully-resolved types" or "types without free type parameters."


Overall: the fix is correct and safe. The conservatism is acceptable. Nice work.

@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label May 25, 2026
@T-Gro T-Gro self-requested a review May 25, 2026 11:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-reviewed PR reviewed by AI review council AI-Tooling-Check-Bypassed Tooling check: non-fork PR, not diff-analyzed

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

Unchecked.defaultof prevents other optimizations from working

2 participants