Resolve ILVerify errors — adjust codegen (551 → 56 errors)#19372
Open
Resolve ILVerify errors — adjust codegen (551 → 56 errors)#19372
Conversation
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>
6cf2d68 to
8d4513d
Compare
Contributor
❗ Release notes required
|
- 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>
Member
|
Nice, finally! |
vzarytovskii
approved these changes
Mar 2, 2026
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>
Member
Author
|
It was for mono. (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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/ldelemfor 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 likeboolandchardiffers from the stack representation. This causesStackUnexpectederrors.Fix: Map primitive ILTypes to their corresponding
ILBasicTypeand 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.fsWave 2a:
callvirt→callon value types (IlxGen.fs)Problem: The codegen emitted
callvirtfor method calls on unboxed value types (e.g.,struct.GetHashCode()). ECMA-335 requirescallorconstrained callvirtfor value type methods — plaincallvirtis invalid on an unboxed value type receiver.Fix: Guard
callvirtemission withboxity <> AsValuein bothGenAppandGenILCall, falling through tocallfor value types.Files:
src/Compiler/CodeGen/IlxGen.fsWave 2b:
castclassat 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.Objectinstead of the interface. This causesStackUnexpectedat the consumer of the join point result.Fix: Introduce a
CastThenBrsequel that emitscastclassto 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.fsWave 3: Filter → catch inside finally/fault handlers (
IlxGen.fs)Problem:
try/withusing awhenguard inside afinallyblock 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, causingStackUnderflow. See dotnet/runtime#112406.Fix: Track
insideFinallyOrFaultHandlerin 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.fsWave 4: Witness field alignment in state machine structs (
IlxGen.fs)Problem:
GenStructStateMachineinitialized closure fields by zippingcloFreeVarswithilCloAllFreeVars. When SRTP witnesses are present,ilCloAllFreeVarshas witness fields prepended, so the zip was misaligned — witness fields got regular free var values and vice versa. This produced type mismatches (StackUnexpected) intask { let! ... and! ... }expressions.Fix: Initialize witness fields separately (first N entries of
ilCloAllFreeVars), then zip the remaining entries withcloFreeVars.Files:
src/Compiler/CodeGen/IlxGen.fsILVerify error count
Remaining 56 errors are all
Unmanaged pointers are not a verifiable type/StackUnexpectedonretype/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