Skip to content

Resolve ILVerify errors — adjust codegen (551 → 56 errors)#19372

Open
T-Gro wants to merge 15 commits intomainfrom
eng/tackle-ilverify-errors
Open

Resolve ILVerify errors — adjust codegen (551 → 56 errors)#19372
T-Gro wants to merge 15 commits intomainfrom
eng/tackle-ilverify-errors

Conversation

@T-Gro
Copy link
Member

@T-Gro T-Gro commented Mar 2, 2026

For a long time, the F# compiler was producing IL code that did not pass ILVerify. This time ends now :)

This PR fixes the vast majority of ILVerify errors across FSharp.Compiler.Service and FSharp.Core (551 → 56), by adjusting IL codegen in 5 targeted waves. The 56 remaining errors are all native pointer / unmanaged memory operations (NativePtr, RawByteMemory, castToString, retype) — inherently unverifiable by design.

Each codegen change includes benchmarks comparing code compiled by the local (patched) compiler vs the SDK compiler, confirming no runtime regressions.


Wave 1: Specialized stelem/ldelem for primitive types (ilwrite.fs)

Problem: The IL writer emitted stelem <TypeToken> / ldelem <TypeToken> for primitive array element types (bool, char, int32, etc.). ILVerify's type verification treats the loaded/stored values as the verification type of the token, which for small integers like bool and char differs from the stack representation. This causes StackUnexpected errors.

Fix: Map primitive ILTypes to their corresponding ILBasicType and emit specialized instructions (stelem.i4, ldelem.u2, etc.) which have well-defined verification semantics. This is the largest single wave — it eliminated errors across the entire compiler and FSharp.Core.

Files: src/Compiler/AbstractIL/ilwrite.fs

Wave 2a: callvirtcall on value types (IlxGen.fs)

Problem: The codegen emitted callvirt for method calls on unboxed value types (e.g., struct.GetHashCode()). ECMA-335 requires call or constrained callvirt for value type methods — plain callvirt is invalid on an unboxed value type receiver.

Fix: Guard callvirt emission with boxity <> AsValue in both GenApp and GenILCall, falling through to call for value types.

Files: src/Compiler/CodeGen/IlxGen.fs

Wave 2b: castclass at interface join points (IlxGen.fs)

Problem: When a match expression returns an interface type and branches produce different concrete classes, the IL verifier computes the LUB of the concrete types at the join point as System.Object instead of the interface. This causes StackUnexpected at the consumer of the join point result.

Fix: Introduce a CastThenBr sequel that emits castclass to the interface type before branching to the join label. Applied only when the result type is an interface and all branches produce reference types.

Files: src/Compiler/CodeGen/IlxGen.fs

Wave 3: Filter → catch inside finally/fault handlers (IlxGen.fs)

Problem: try/with using a when guard inside a finally block emitted a filter block nested inside the finally handler. ILVerify (incorrectly) computes the entry stack for such a filter as empty (the finally entry) instead of having the exception object on stack, causing StackUnderflow. See dotnet/runtime#112406.

Fix: Track insideFinallyOrFaultHandler in the codegen environment and suppress filter block emission inside finally/fault handlers, falling back to a catch-based pattern instead.

Files: src/Compiler/CodeGen/IlxGen.fs

Wave 4: Witness field alignment in state machine structs (IlxGen.fs)

Problem: GenStructStateMachine initialized closure fields by zipping cloFreeVars with ilCloAllFreeVars. When SRTP witnesses are present, ilCloAllFreeVars has witness fields prepended, so the zip was misaligned — witness fields got regular free var values and vice versa. This produced type mismatches (StackUnexpected) in task { let! ... and! ... } expressions.

Fix: Initialize witness fields separately (first N entries of ilCloAllFreeVars), then zip the remaining entries with cloFreeVars.

Files: src/Compiler/CodeGen/IlxGen.fs


ILVerify error count

Baseline Before After
FSharp.Compiler.Service Debug net10.0 73 11
FSharp.Compiler.Service Debug netstandard2.0 98 11
FSharp.Compiler.Service Release net10.0 99 12
FSharp.Compiler.Service Release netstandard2.0 125 12
FSharp.Core Debug netstandard2.0 42 5
FSharp.Core Debug netstandard2.1 42 5
FSharp.Core Release netstandard2.0 36 0
FSharp.Core Release netstandard2.1 36 0
Total 551 56

Remaining 56 errors are all Unmanaged pointers are not a verifiable type / StackUnexpected on retype/castToString/castclassPrim/iscastPrim/notnullPrim — intrinsically unverifiable operations.

Benchmark results (LocalCompiler = this PR, SdkCompiler = baseline)

No regression: stelem/ldelem (0.99–1.03, within noise), task CEs, callvirt→call (StructToString, StructEquals, StructInDictionary)
Slight improvement: callvirt→call on GetHashCode (0.93–0.96), CastThenBr on interface match (0.88–0.95), filter→catch on exception paths (0.93–0.96)
Slight regression: filter→catch on no-exception path (1.06)

Full BenchmarkDotNet output

BenchmarkDotNet v0.13.10, macOS 26.2 (25C56) [Darwin 25.2.0]
Apple M3 Max, 1 CPU, 16 logical and 16 physical cores
.NET SDK 10.0.101
  [Host]        : .NET 10.0.1 (10.0.125.57005), Arm64 RyuJIT AdvSIMD DEBUG
  LocalCompiler : .NET 10.0.2 (10.0.225.61305), Arm64 RyuJIT AdvSIMD
  SdkCompiler   : .NET 10.0.2 (10.0.225.61305), Arm64 RyuJIT AdvSIMD

Arguments=/p:BUILDING_USING_DOTNET=true  

Type Method Job Mean Error StdDev Median P95 Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
CallVirtOnValueTypeBenchmark StructGetHashCode LocalCompiler 3,011.739 ns 4.1497 ns 3.8816 ns 3,011.152 ns 3,018.224 ns 1.00 0.00 - - - NA
CallVirtOnValueTypeBenchmark StructGetHashCode SdkCompiler 3,020.885 ns 3.4751 ns 2.9019 ns 3,021.219 ns 3,024.821 ns 1.00 0.00 - - - NA
CastThenBrBenchmark MatchReturningInterface_NoAlloc LocalCompiler 23,985.794 ns 475.2417 ns 794.0221 ns 23,652.795 ns 25,443.705 ns 0.88 0.06 - - - NA
CastThenBrBenchmark MatchReturningInterface_NoAlloc SdkCompiler 27,398.117 ns 543.5979 ns 1,432.0542 ns 27,033.290 ns 30,303.813 ns 1.00 0.00 - - - NA
FilterInFinallyBenchmark TryWithInFinally_NoException LocalCompiler 555.539 ns 10.4836 ns 17.2249 ns 548.821 ns 586.716 ns 1.06 0.04 - - - NA
FilterInFinallyBenchmark TryWithInFinally_NoException SdkCompiler 530.672 ns 1.7119 ns 1.4295 ns 530.475 ns 533.055 ns 1.00 0.00 - - - NA
StelemLdelemBenchmark BoolArrayReadWrite LocalCompiler 3,589.626 ns 24.9488 ns 20.8334 ns 3,587.869 ns 3,625.366 ns 1.02 0.01 - - - NA
StelemLdelemBenchmark BoolArrayReadWrite SdkCompiler 3,513.090 ns 40.1561 ns 37.5620 ns 3,536.126 ns 3,547.690 ns 1.00 0.00 - - - NA
TaskMergeSourcesBenchmark TaskLetBangAndBang LocalCompiler 14.754 ns 0.2272 ns 0.2125 ns 14.692 ns 15.123 ns 1.00 0.02 0.0004 - 72 B 1.00
TaskMergeSourcesBenchmark TaskLetBangAndBang SdkCompiler 14.751 ns 0.0665 ns 0.0589 ns 14.737 ns 14.860 ns 1.00 0.00 0.0004 - 72 B 1.00
CallVirtOnValueTypeBenchmark StructToString LocalCompiler 6,550.059 ns 30.8829 ns 28.8879 ns 6,552.014 ns 6,596.813 ns 1.00 0.00 0.1678 - 27960 B 1.00
CallVirtOnValueTypeBenchmark StructToString SdkCompiler 6,522.629 ns 22.4117 ns 17.4976 ns 6,524.147 ns 6,548.854 ns 1.00 0.00 0.1678 - 27960 B 1.00
CastThenBrBenchmark MatchReturningInterface_Alloc LocalCompiler 57,340.381 ns 566.1241 ns 501.8542 ns 57,160.351 ns 58,333.448 ns 1.02 0.01 1.5869 - 266456 B 1.00
CastThenBrBenchmark MatchReturningInterface_Alloc SdkCompiler 56,407.777 ns 807.8752 ns 755.6869 ns 56,003.489 ns 57,572.996 ns 1.00 0.00 1.5869 - 266456 B 1.00
FilterInFinallyBenchmark TryWithInFinally_WithException LocalCompiler 2,140,943.877 ns 42,143.2213 ns 51,755.6605 ns 2,130,150.148 ns 2,226,091.349 ns 0.93 0.07 - - 216000 B 1.00
FilterInFinallyBenchmark TryWithInFinally_WithException SdkCompiler 2,262,674.964 ns 56,794.0958 ns 151,594.9387 ns 2,201,999.594 ns 2,728,839.813 ns 1.00 0.00 - - 216003 B 1.00
StelemLdelemBenchmark IntArrayReadWrite LocalCompiler 2,884.971 ns 9.4347 ns 8.3636 ns 2,885.090 ns 2,896.610 ns 1.03 0.00 - - - NA
StelemLdelemBenchmark IntArrayReadWrite SdkCompiler 2,812.112 ns 6.3549 ns 5.3067 ns 2,811.917 ns 2,820.260 ns 1.00 0.00 - - - NA
TaskMergeSourcesBenchmark TaskLetBangAndBang3 LocalCompiler 27.341 ns 0.3246 ns 0.2711 ns 27.374 ns 27.714 ns 1.01 0.01 0.0009 - 152 B 1.00
TaskMergeSourcesBenchmark TaskLetBangAndBang3 SdkCompiler 27.020 ns 0.2093 ns 0.1748 ns 27.045 ns 27.238 ns 1.00 0.00 0.0009 - 152 B 1.00
CallVirtOnValueTypeBenchmark StructEquals LocalCompiler 29,573.711 ns 220.8120 ns 206.5476 ns 29,562.723 ns 29,922.468 ns 1.01 0.02 1.4343 - 239976 B 1.00
CallVirtOnValueTypeBenchmark StructEquals SdkCompiler 29,420.301 ns 390.6764 ns 365.4390 ns 29,608.037 ns 29,790.980 ns 1.00 0.00 1.4343 - 239976 B 1.00
CastThenBrBenchmark MatchReturningIComparable LocalCompiler 91,689.908 ns 369.9182 ns 308.8985 ns 91,709.345 ns 92,134.706 ns 0.95 0.04 0.8545 - 159264 B 1.00
CastThenBrBenchmark MatchReturningIComparable SdkCompiler 95,445.088 ns 1,708.9753 ns 3,413.0083 ns 93,995.697 ns 103,541.611 ns 1.00 0.00 0.8545 - 159264 B 1.00
FilterInFinallyBenchmark TryWithInFinally_GuardHit LocalCompiler 1,972,895.145 ns 8,926.4190 ns 7,453.9641 ns 1,969,982.098 ns 1,984,866.016 ns 0.96 0.01 - - 224000 B 1.00
FilterInFinallyBenchmark TryWithInFinally_GuardHit SdkCompiler 2,068,823.482 ns 26,346.7064 ns 24,644.7255 ns 2,058,629.232 ns 2,102,062.206 ns 1.00 0.00 - - 224001 B 1.00
StelemLdelemBenchmark CharArrayReadWrite LocalCompiler 3,837.618 ns 7.9594 ns 6.6464 ns 3,834.811 ns 3,848.598 ns 0.99 0.00 - - - NA
StelemLdelemBenchmark CharArrayReadWrite SdkCompiler 3,884.815 ns 19.1506 ns 15.9916 ns 3,878.658 ns 3,914.045 ns 1.00 0.00 - - - NA
TaskMergeSourcesBenchmark TaskLetBangSequential LocalCompiler 5.368 ns 0.1307 ns 0.1790 ns 5.324 ns 5.703 ns 1.00 0.06 - - - NA
TaskMergeSourcesBenchmark TaskLetBangSequential SdkCompiler 5.382 ns 0.1341 ns 0.1435 ns 5.359 ns 5.664 ns 1.00 0.00 - - - NA
CallVirtOnValueTypeBenchmark StructInDictionary LocalCompiler 12,207.604 ns 142.8089 ns 133.5836 ns 12,157.128 ns 12,412.242 ns 1.00 0.01 0.6866 0.0916 111648 B 1.00
CallVirtOnValueTypeBenchmark StructInDictionary SdkCompiler 12,189.143 ns 148.7887 ns 165.3783 ns 12,107.841 ns 12,486.912 ns 1.00 0.00 0.6866 0.0916 111648 B 1.00
FilterInFinallyBenchmark SimpleTryFinally LocalCompiler 530.156 ns 1.2822 ns 1.0707 ns 530.444 ns 531.379 ns 0.96 0.01 - - - NA
FilterInFinallyBenchmark SimpleTryFinally SdkCompiler 552.759 ns 10.7679 ns 8.9917 ns 548.853 ns 569.225 ns 1.00 0.00 - - - NA
StelemLdelemBenchmark SByteArrayReadWrite LocalCompiler 5,512.868 ns 28.2968 ns 26.4689 ns 5,510.768 ns 5,560.457 ns 0.99 0.01 - - - NA
StelemLdelemBenchmark SByteArrayReadWrite SdkCompiler 5,576.221 ns 76.5248 ns 63.9016 ns 5,575.136 ns 5,679.119 ns 1.00 0.00 - - - NA
TaskMergeSourcesBenchmark TaskSimple LocalCompiler 6.829 ns 0.0918 ns 0.0859 ns 6.832 ns 6.976 ns 1.01 0.02 0.0004 - 72 B 1.00
TaskMergeSourcesBenchmark TaskSimple SdkCompiler 6.746 ns 0.1331 ns 0.1112 ns 6.704 ns 6.943 ns 1.00 0.00 0.0004 - 72 B 1.00
CallVirtOnValueTypeBenchmark IntGetHashCode LocalCompiler 2,504.634 ns 6.6625 ns 5.9061 ns 2,503.363 ns 2,513.041 ns 0.93 0.00 - - - NA
CallVirtOnValueTypeBenchmark IntGetHashCode SdkCompiler 2,692.462 ns 6.0291 ns 5.3446 ns 2,692.454 ns 2,700.421 ns 1.00 0.00 - - - NA
StelemLdelemBenchmark ByteArrayReadWrite LocalCompiler 3,827.056 ns 20.1288 ns 16.8085 ns 3,833.095 ns 3,837.618 ns 1.02 0.00 - - - NA
StelemLdelemBenchmark ByteArrayReadWrite SdkCompiler 3,742.817 ns 8.5823 ns 7.1666 ns 3,745.259 ns 3,752.045 ns 1.00 0.00 - - - NA
CallVirtOnValueTypeBenchmark DateTimeGetHashCode LocalCompiler 2,629.997 ns 38.8126 ns 34.4064 ns 2,637.633 ns 2,670.062 ns 0.96 0.01 - - - NA
CallVirtOnValueTypeBenchmark DateTimeGetHashCode SdkCompiler 2,724.887 ns 10.5121 ns 8.7781 ns 2,720.738 ns 2,741.800 ns 1.00 0.00 - - - NA
StelemLdelemBenchmark IntArrayFilterToArray LocalCompiler 7,796.051 ns 23.8529 ns 21.1450 ns 7,796.818 ns 7,825.524 ns 0.99 0.01 0.1221 - 21296 B 1.00
StelemLdelemBenchmark IntArrayFilterToArray SdkCompiler 7,882.273 ns 125.4244 ns 111.1855 ns 7,827.736 ns 8,067.406 ns 1.00 0.00 0.1221 - 21296 B 1.00
StelemLdelemBenchmark BoolArrayCountTrue LocalCompiler 2,800.847 ns 17.0188 ns 15.0868 ns 2,796.306 ns 2,828.988 ns 1.00 0.01 - - - NA
StelemLdelemBenchmark BoolArrayCountTrue SdkCompiler 2,809.914 ns 22.2283 ns 20.7924 ns 2,801.633 ns 2,849.625 ns 1.00 0.00 - - - NA

T-Gro and others added 9 commits March 2, 2026 10:27
When emitting IL for I_stelem_any and I_ldelem_any with primitive
element types (bool, byte, char, int16, int32, etc.), emit the
specialized instructions (stelem.i1, stelem.i4, ldelem.u1, etc.)
instead of the generic stelem <T> / ldelem <T> form.

This matches C# compiler behavior and eliminates 306 ILVerify
StackUnexpected errors caused by asymmetric verification type
handling in ILVerify (ECMA-335 §I.8.7).

ILVerify baseline changes:
- FSharp.Core: 32 → 20 errors (Release), 37 → 25 (Debug)
- FSharp.Compiler.Service: 125 → 18 (Release ns2.0), 99 → 18 (net10)
  98 → 16 (Debug ns2.0), 73 → 16 (Debug net10)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ange struct GetHashCode

- HashMultiMap: Add explicit :> IDictionary<_,_> upcasts in both branches of
  the if/else expressions for firstEntries and rest fields. This causes the
  F# compiler to emit castclass at the join point, which ILVerify needs to
  see the correct interface type on the stack instead of computing the LUB
  of ConcurrentDictionary and Dictionary as System.Object.

- range.fs: Change o.GetHashCode() to (box o).GetHashCode() in the Range
  struct IEqualityComparer. Range is a struct, and callvirt on a value type
  without a constrained. prefix is flagged by ILVerify as CallVirtOnValueType.
  Boxing first makes the callvirt valid on the boxed reference type.

Together these eliminate 5 ILVerify baseline entries (2 HashMultiMap + 1
AssemblyResolveHandler + 1 FSharpChecker + 1 CallVirtOnValueType).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…st and Range struct GetHashCode"

This reverts commit 4614ffe.
… casts

Two codegen fixes to eliminate ILVerify errors:

1. CallVirtOnValueType: In GenApp, prevent emitting callvirt when the
   declaring type is a value type (boxity = AsValue). For value types,
   callvirt without constrained. prefix is invalid per ECMA-335. This
   also applies as a safety net in GenILCall.

2. Interface join point casts: Add CastThenBr sequel that emits
   castclass before branching at match/if-else join points when the
   result type is an interface. Without this, ILVerify computes the LUB
   of different concrete class types as System.Object instead of the
   expected interface type. Only applied when all branches produce
   reference types (not value types or unit).

Eliminates 5 ILVerify baseline entries per configuration:
- HashMultiMap StackUnexpected x2 (IDictionary join)
- AssemblyResolveHandler StackUnexpected (IDisposable join)
- FSharpChecker StackUnexpected (IBackgroundCompiler join)
- comparer@607 CallVirtOnValueType (Range.GetHashCode)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ILVerify has a bug where a filter nested inside a finally handler
gets its entry stack computed incorrectly (as empty instead of
[exception_object]). This happens because ILVerify's
FindEnclosingExceptionRegions picks HandlerIndex (the enclosing
finally) instead of FilterIndex when both are set, then uses the
finally's Kind to determine stack state.

The fix adds insideFinallyOrFaultHandler tracking to IlxGenEnv.
When inside a finally handler, GenTryWith emits catch blocks
instead of filter blocks. Semantically identical for catch-all
patterns (filter always returns 1 == catch Exception).

Eliminates the DoWithColor StackUnderflow error:
- FCS Release: 13 -> 12 errors per config
- FCS Debug: 11 -> 10 errors per config (to be verified by CI)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GenStructStateMachine zipped cloFreeVars (regular free vars only) with
ilCloAllFreeVars (witness fields + regular fields). When witness fields
were present (in $W methods), the zip was misaligned: regular var values
were stored into witness fields and vice versa.

The fix initializes witness fields separately first, then skips them when
zipping with regular free vars.

This was a genuine codegen bug affecting all MergeSources$W methods in
FSharp.Core task computation expressions. The $W state machine struct had
both witness function fields (getAwaiter, getResult, get_IsCompleted) and
regular value fields (computation, task), but the initialization code
stored task values into witness fields due to the array offset mismatch.

Eliminates all 20 FSharp.Core Release ILVerify errors and all 20
MergeSources$W errors from Debug baselines.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
BDN benchmarks for each IL codegen change:
- StelemLdelem: primitive array read/write (ldelem.u1, stelem.i4, etc.)
- CallVirtOnValueType: struct GetHashCode/ToString/Equals (call vs callvirt)
- CastThenBr: match returning interface types (castclass at join points)
- FilterInFinally: try/with inside finally (catch vs filter)

Results: no regressions. callvirt→call shows 4-7% improvement for
struct method calls. All other changes performance-neutral.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@T-Gro T-Gro force-pushed the eng/tackle-ilverify-errors branch from 6cf2d68 to 8d4513d Compare March 2, 2026 09:29
@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 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/10.0.300.md

@T-Gro T-Gro changed the title Resolve ilverify errors - adjust codegen Resolve ILVerify errors — adjust codegen (551 → 56 errors) Mar 2, 2026
T-Gro and others added 2 commits March 2, 2026 10:43
- TailCall 06: Update IL offsets (IL_0040→IL_0034) due to stelem.i4 being shorter
- ILVerify Debug baselines: Fix set_Item offset (0x1F→0x25), normalize pointerToNativeInt
- ILVerify script: Handle empty baseline matching empty output (FSharp.Core Release)
- StructDU net472 baseline: callvirt→call on value type ToString()
- Trimming check: Update expected FSharp.Core.dll size (311296→311808)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@vzarytovskii
Copy link
Member

Nice, finally!

@github-project-automation github-project-automation bot moved this from New to In Progress in F# Compiler and Tooling Mar 2, 2026
@vzarytovskii
Copy link
Member

IIRC (I don't have data to support that, only old discussions I had with Don and Kevin), exception filters resluted in some worse runtime perf on old runtimes, not sure if we care or care to confrim/deny that.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@T-Gro
Copy link
Member Author

T-Gro commented Mar 2, 2026

It was for mono.
The F.C.S itself is using them in the last 1-2 stable versions without complaints.

(i.e. no reported problems on the environments where people run the compiler/SDK. Which of course does not mean all environments where the compiled code can run)

UPDATE: The filter blocks are only default for the compiler itself, it is a compiler switch. Changing it to be come default for all users is a different story, not done here.

@T-Gro T-Gro marked this pull request as ready for review March 2, 2026 16:55
@T-Gro T-Gro requested a review from a team as a code owner March 2, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants