Skip to content

Commit b957b25

Browse files
T-GroCopilot
andcommitted
Fix NRE when calling virtual Object methods on value types via inline SRTP (#8098)
When an inline SRTP function calls a virtual Object method (ToString, GetHashCode, Equals) on a value type, the compiler emits a constrained callvirt instruction. ECMA-335 requires the receiver of a constrained call to be a managed pointer (&), but the codegen was pushing the raw value (or reference) directly onto the stack. Root cause: MakeMethInfoCall (used by SRTP trait resolution) computes PossibleConstrainedCall but never takes the receiver's address — unlike TakeObjAddrForMethodCall which handles this correctly in the type-checker path for direct calls. Fix: In MakeMethInfoCall, when ComputeConstrainedCallInfo returns Some (indicating a constrained call is needed), take the address of the receiver via mkExprAddrOfExpr before building the IL call expression. This mirrors TakeObjAddrForMethodCall and ensures the expression tree contains the proper address-of wrapper, so GenILCall receives a managed pointer on the stack as ECMA-335 requires. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a21c12f commit b957b25

File tree

3 files changed

+108
-3
lines changed

3 files changed

+108
-3
lines changed

docs/release-notes/.FSharp.Compiler.Service/11.0.100.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
### Fixed
22

3+
* Fix NullReferenceException when calling virtual Object methods (ToString, GetHashCode, Equals) on value types through inline SRTP functions. The codegen now ensures the receiver is passed as a managed pointer for constrained callvirt instructions, as required by ECMA-335. ([Issue #8098](https://github.com/dotnet/fsharp/issues/8098), [PR #NNNN](https://github.com/dotnet/fsharp/pull/NNNN))
34
* Fix CLIEvent properties to be correctly recognized as events: `IsEvent` returns `true` and `XmlDocSig` uses `E:` prefix instead of `P:`. ([Issue #10273](https://github.com/dotnet/fsharp/issues/10273), [PR #18584](https://github.com/dotnet/fsharp/pull/18584))
45
* Fix extra sequence point at the end of match expressions. ([Issue #12052](https://github.com/dotnet/fsharp/issues/12052), [PR #19278](https://github.com/dotnet/fsharp/pull/19278))
56
* Fix wrong sequence point range for `return`/`yield`/`return!`/`yield!` inside computation expressions. ([Issue #19248](https://github.com/dotnet/fsharp/issues/19248), [PR #19278](https://github.com/dotnet/fsharp/pull/19278))

src/Compiler/Checking/MethodCalls.fs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2242,9 +2242,13 @@ let GenWitnessExpr amap g m (traitInfo: TraitConstraintInfo) argExprs =
22422242
| Some r -> r :: convertedArgs
22432243
| None -> convertedArgs
22442244

2245-
// Fix bug 1281: If we resolve to an instance method on a struct and we haven't yet taken
2246-
// the address of the object then go do that
2247-
if minfo.IsStruct && minfo.IsInstance then
2245+
// Fix bug 1281 / issue #8098: If the receiver needs its address taken for a
2246+
// constrained call, go do that and re-resolve via TraitCall with the byref receiver.
2247+
let needsAddrTaken =
2248+
minfo.IsInstance &&
2249+
(minfo.IsStruct || (ComputeConstrainedCallInfo g amap m staticTyOpt argExprs minfo).IsSome)
2250+
2251+
if needsAddrTaken then
22482252
match argExprs with
22492253
| h :: t when not (isByrefTy g (tyOfExpr g h)) ->
22502254
let wrap, h', _readonly, _writeonly = mkExprAddrOfExpr g true false PossiblyMutates h None m

tests/FSharp.Compiler.ComponentTests/Conformance/Types/TypeConstraints/IWSAMsAndSRTPs/IWSAMsAndSRTPsTests.fs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1831,3 +1831,103 @@ let _x3 = curryN f3 1 2 3
18311831
FSharp "module T\nopen CsLib\nlet _ = IP.Get()" |> asExe |> withOptions iwsamWarnings |> withReferences [csLib]
18321832
|> compileAndRun |> shouldSucceed
18331833

1834+
// https://github.com/dotnet/fsharp/issues/8098
1835+
[<Fact>]
1836+
let ``Issue 8098 - ToString on int via inline SRTP does not throw NRE`` () =
1837+
FSharp """
1838+
module Test
1839+
1840+
let inline toString (x: ^a) = (^a : (member ToString : unit -> string) x)
1841+
1842+
[<EntryPoint>]
1843+
let main _ =
1844+
let s = toString 123
1845+
if s <> "123" then failwith (sprintf "Expected '123' but got '%s'" s)
1846+
0
1847+
"""
1848+
|> asExe
1849+
|> compileExeAndRun
1850+
|> shouldSucceed
1851+
1852+
// https://github.com/dotnet/fsharp/issues/8098
1853+
[<Fact>]
1854+
let ``Issue 8098 - GetHashCode on int via inline SRTP does not throw NRE`` () =
1855+
FSharp """
1856+
module Test
1857+
1858+
let inline getHash (x: ^a) = (^a : (member GetHashCode : unit -> int) x)
1859+
1860+
[<EntryPoint>]
1861+
let main _ =
1862+
let h = getHash 42
1863+
if h <> 42 then failwith (sprintf "Expected 42 but got %d" h)
1864+
0
1865+
"""
1866+
|> asExe
1867+
|> compileExeAndRun
1868+
|> shouldSucceed
1869+
1870+
// https://github.com/dotnet/fsharp/issues/8098
1871+
[<Fact>]
1872+
let ``Issue 8098 - ToString on custom struct via inline SRTP does not throw NRE`` () =
1873+
FSharp """
1874+
module Test
1875+
1876+
[<Struct>]
1877+
type MyPoint = { X: int; Y: int }
1878+
1879+
let inline toString (x: ^a) = (^a : (member ToString : unit -> string) x)
1880+
1881+
[<EntryPoint>]
1882+
let main _ =
1883+
let p = { X = 1; Y = 2 }
1884+
let s = toString p
1885+
if s = null then failwith "Got null"
1886+
0
1887+
"""
1888+
|> asExe
1889+
|> compileExeAndRun
1890+
|> shouldSucceed
1891+
1892+
// https://github.com/dotnet/fsharp/issues/8098
1893+
[<Fact>]
1894+
let ``Issue 8098 - ToString on reference type via inline SRTP still works`` () =
1895+
FSharp """
1896+
module Test
1897+
1898+
let inline toString (x: ^a) = (^a : (member ToString : unit -> string) x)
1899+
1900+
[<EntryPoint>]
1901+
let main _ =
1902+
let s = toString "hello"
1903+
if s <> "hello" then failwith (sprintf "Expected 'hello' but got '%s'" s)
1904+
0
1905+
"""
1906+
|> asExe
1907+
|> compileExeAndRun
1908+
|> shouldSucceed
1909+
1910+
// https://github.com/dotnet/fsharp/issues/8098
1911+
[<Fact>]
1912+
let ``Issue 8098 - ToString on struct without override via inline SRTP does not throw NRE`` () =
1913+
FSharp """
1914+
module Test
1915+
1916+
[<Struct>]
1917+
type EmptyStruct =
1918+
val X: int
1919+
new(x) = { X = x }
1920+
// No ToString override — inherits Object.ToString()
1921+
1922+
let inline toString (x: ^a) = (^a : (member ToString : unit -> string) x)
1923+
1924+
[<EntryPoint>]
1925+
let main _ =
1926+
let s = toString (EmptyStruct(42))
1927+
if s = null then failwith "Got null"
1928+
0
1929+
"""
1930+
|> asExe
1931+
|> compileExeAndRun
1932+
|> shouldSucceed
1933+

0 commit comments

Comments
 (0)