Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -34,6 +34,7 @@
* 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))
* Fix multiline nested type arguments failing to parse when the closing `>` aligns with the opening type name's column. ([Issue #15171](https://github.com/dotnet/fsharp/issues/15171))
* Remove misleading FS0193 when record fields differ in order between a signature and its implementation, while retaining FS0312. ([Issue #20410](https://github.com/dotnet/fsharp/issues/20410), [PR #20559](https://github.com/dotnet/fsharp/pull/20559))
* Preserve outer rethrows and emit legal IL for nested runtime-async `try/with` after suspension. ([Issue #20575](https://github.com/dotnet/fsharp/issues/20575), [PR #20586](https://github.com/dotnet/fsharp/pull/20586))
* Tooltip "Full name" now shows demangled companion module names (e.g. `MyType.func` instead of `MyTypeModule.func`). ([Issue #17335](https://github.com/dotnet/fsharp/issues/17335), [PR #19867](https://github.com/dotnet/fsharp/pull/19867))
* Fix spurious FS0410 accessibility error when tuple-deconstructing bindings use private types in the same module scope. ([Issue #4161](https://github.com/dotnet/fsharp/issues/4161), [PR #19947](https://github.com/dotnet/fsharp/pull/19947))
* Fix internal error (FS0193) when calling an indexed property setter with a named argument that matches an indexer parameter. ([Issue #16034](https://github.com/dotnet/fsharp/issues/16034), [PR #19851](https://github.com/dotnet/fsharp/pull/19851))
Expand Down
13 changes: 7 additions & 6 deletions src/Compiler/Optimize/RuntimeAsyncExceptionRewrite.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ let private RuntimeAsyncChoiceCase g m ty caseIndex expr =

let private RuntimeAsyncReraise m resultTy exnExpr = mkThrow m resultTy exnExpr

let private RewriteRuntimeAsyncReraise g resultTy handlerVal handler =
let private RewriteRuntimeAsyncReraise g handlerVal handler =
RewriteExpr
{
PreIntercept =
Some(fun _ expr ->
Some(fun recurse expr ->
match stripExpr expr with
| TryWithExpr _ -> Some expr
| Expr.Op(TOp.Reraise, _, _, m) -> Some(mkThrow m resultTy (exprForVal m handlerVal))
| TryWithExpr(spTry, spWith, nestedTy, body, filterVal, filter, innerHandlerVal, innerHandler, m) ->
Some(mkTryWith g (recurse body, filterVal, filter, innerHandlerVal, innerHandler, m, nestedTy, spTry, spWith))
| Expr.Op(TOp.Reraise, _, _, m) -> Some(mkThrow m (tyOfExpr g expr) (exprForVal m handlerVal))
| Expr.App(Expr.Val(vref, _, m), _, _, _, _) when valRefEq g vref g.reraise_vref ->
Some(mkThrow m resultTy (exprForVal m handlerVal))
Some(mkThrow m (tyOfExpr g expr) (exprForVal m handlerVal))
| _ -> None)
PreInterceptBinding = None
PostTransform = (fun _ -> None)
Expand Down Expand Up @@ -101,7 +102,7 @@ let RewriteRuntimeAsyncExceptionHandlers (g: TcGlobals) expr =
| TryWithExpr(_, _, resultTy, body, _, _, handlerVal, handler, m) when IsRuntimeAsyncExceptionHandler analyzer expr ->
Some(
rewriteCapturedException m resultTy body (fun bodySucceeded bodyValue exceptionExpr ->
let handler = RewriteRuntimeAsyncReraise g resultTy handlerVal handler
let handler = RewriteRuntimeAsyncReraise g handlerVal handler

let handler = mkCompGenLet m handlerVal exceptionExpr handler

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
module Reraise

open System
open System.IO
open System.Threading.Tasks
open System.Runtime.CompilerServices
open Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers

let equal expected actual =
if actual <> expected then failwith $"Expected {expected}, got {actual}"

let same expected actual =
if not (obj.ReferenceEquals(expected, actual)) then failwith "Exception/result identity changed"

let observe action =
try Ok(action ()) with error -> Error error

let checkOutcome original expected outcome =
match expected, outcome with
| Some value, Ok actual -> equal value actual
| None, Error error -> same original error
| _ -> failwith $"Unexpected outcome: {outcome}"

let guard (trace: ResizeArray<string>) name value =
trace.Add name
value

let synchronousSelection (original: exn) fallback (trace: ResizeArray<string>) =
try
raise original
with _ ->
try
reraise ()
with
| :? IOException when guard trace "false" false -> failwith "False guard selected"
| :? IOException as caught when guard trace "true" true ->
same original caught
trace.Add "handler"
7
| caught when guard trace "fallback" fallback ->
same original caught
trace.Add "fallback-handler"
-1

let synchronousCleanup (original: exn) (trace: ResizeArray<string>) =
try
raise original
with _ ->
try
try
try reraise () finally trace.Add "inner-finally"
with :? IOException as caught ->
same original caught
trace.Add "handler"
7
finally
trace.Add "outer-finally"

[<NoCompilerInlining>]
let Await () = 0x00001AFE

let filteredReraise (original: exn) (trace: ResizeArray<string>) =
try raise original
with caught when guard trace "filter" true ->
same original caught
trace.Add(string (Await ()))
reraise ()

#if !CONTROLS
let recover (original: exn) (audit: Task) : Task<int> =
__runtimeAsyncReturn (
try
raise original
with _ ->
AsyncHelpers.Await audit
try
reraise ()
with :? IOException ->
7)

#if MATRIX
let recoverString (original: exn) (audit: Task) (trace: ResizeArray<string>) : Task<string> =
__runtimeAsyncReturn (
try raise original
with _ ->
trace.Add "await"
AsyncHelpers.Await audit
let n: int =
try reraise ()
with :? IOException as caught ->
same original caught
7
string n)

let recoverValue (original: exn) (audit: Task) (trace: ResizeArray<string>) (value: 'T) : ValueTask<'T> =
__runtimeAsyncReturnValueTask (
try raise original
with _ ->
trace.Add "await"
AsyncHelpers.Await audit
let n: int =
try reraise ()
with :? IOException as caught ->
same original caught
7
equal 7 n
value)

let innerOwner (outer: exn) (inner: exn) (audit: Task) (trace: ResizeArray<string>) : Task<int> =
__runtimeAsyncReturn (
try raise outer
with _ ->
trace.Add "await"
AsyncHelpers.Await audit
try raise inner
with _ -> reraise ())

let selection (original: exn) fallback (audit: Task) (trace: ResizeArray<string>) : Task<int> =
__runtimeAsyncReturn (
try raise original
with _ ->
trace.Add "await"
AsyncHelpers.Await audit
try
reraise ()
with
| :? IOException when guard trace "false" false -> failwith "False guard selected"
| :? IOException as caught when guard trace "true" true ->
same original caught
trace.Add "handler"
7
| caught when guard trace "fallback" fallback ->
same original caught
trace.Add "fallback-handler"
-1)

let cleanup (original: exn) (audit: Task) (trace: ResizeArray<string>) : Task<int> =
__runtimeAsyncReturn (
try raise original
with _ ->
trace.Add "await"
AsyncHelpers.Await audit
try
try
try reraise () finally trace.Add "inner-finally"
with :? IOException as caught ->
same original caught
trace.Add "handler"
7
finally
trace.Add "outer-finally")
#endif

let pending before probe =
let audit = TaskCompletionSource<unit>(TaskCreationOptions.RunContinuationsAsynchronously)
let trace = ResizeArray<string>()
let work: Task<'T> = probe audit.Task trace
equal before (List.ofSeq trace)
if audit.Task.IsCompleted || work.IsCompleted then failwith "Expected pending audit and work"
audit.SetResult(())
let outcome = observe (fun () -> work.WaitAsync(TimeSpan.FromSeconds 30.).GetAwaiter().GetResult())
outcome, List.ofSeq trace
#endif

[<EntryPoint>]
let main _ =
for original in [ IOException() :> exn; InvalidOperationException() ] do
let matching = original :? IOException
let expected = if matching then Some 7 else None
let filteredTrace = ResizeArray<string>()
observe (fun () -> filteredReraise original filteredTrace) |> checkOutcome original None
equal "6910" filteredTrace.[filteredTrace.Count - 1]
if filteredTrace.Count < 2 || Seq.exists ((<>) "filter") (Seq.take (filteredTrace.Count - 1) filteredTrace) then
failwith $"Unexpected filter trace: {filteredTrace}"
let cleanupTrace = ResizeArray<string>()
observe (fun () -> synchronousCleanup original cleanupTrace) |> checkOutcome original expected
equal
(if matching then ["inner-finally"; "handler"; "outer-finally"] else ["inner-finally"; "outer-finally"])
(List.ofSeq cleanupTrace)
for fallback in [false; true] do
let selected = if matching then Some 7 elif fallback then Some -1 else None
let syncTrace = ResizeArray<string>()
observe (fun () -> synchronousSelection original fallback syncTrace) |> checkOutcome original selected
#if MATRIX
let outcome, trace = pending ["await"] (selection original fallback)
checkOutcome original selected outcome
equal ("await" :: List.ofSeq syncTrace) trace
#endif
#if !CONTROLS
pending [] (fun audit _ -> recover original audit) |> fst |> checkOutcome original expected
#endif
#if MATRIX
pending ["await"] (recoverString original) |> fst |> checkOutcome original (Option.map string expected)
let inner = ArgumentException("inner")
pending ["await"] (innerOwner original inner) |> fst |> checkOutcome inner None
let outcome, trace = pending ["await"] (cleanup original)
checkOutcome original expected outcome
equal ("await" :: List.ofSeq cleanupTrace) trace
let valueResult, _ = pending ["await"] (fun audit trace -> (recoverValue original audit trace 42).AsTask())
checkOutcome original (Option.map (fun _ -> 42) expected) valueResult
let reference = obj()
let referenceResult, _ = pending ["await"] (fun audit trace -> (recoverValue original audit trace reference).AsTask())
checkOutcome original (Option.map (fun _ -> reference) expected) referenceResult
match referenceResult with
| Ok actual -> same reference actual
| Error _ -> ()
#endif
0
108 changes: 108 additions & 0 deletions tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsyncTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,114 @@ let main _ =
|> compileExeAndRun
|> shouldSucceed

let private checkReraiseOwnership optimized mode =
let methods =
match mode with
| "CONTROLS" -> [ "synchronousSelection", false; "synchronousCleanup", false; "filteredReraise", false; "Await", false ]
| "PRIMARY" -> [ "recover", true ]
| _ ->
[ "recover", true; "recoverString", true; "recoverValue", true; "innerOwner", true
"selection", true; "cleanup", true ]
let result =
FsFromPath(Path.Combine(__SOURCE_DIRECTORY__, "RuntimeAsync", "RuntimeAsyncReraiseOwnership.fs"))
|> withLangVersionPreview
|> withFSharpCoreShippedNet
|> withOptimization optimized
|> withDefines [mode]
|> asExe
|> compile
|> shouldSucceed

result
|> verifyRuntimeAsyncExceptionRegions (methods |> List.map (fun (name, awaits) -> $"Reraise::{name}", awaits))
|> run
|> shouldSucceed

[<Theory>]
[<InlineData(false)>]
[<InlineData(true)>]
let ``Issue 20575 runtime async nested reraise ownership`` optimized =
checkReraiseOwnership optimized "PRIMARY"

[<Theory>]
[<InlineData(false)>]
[<InlineData(true)>]
let ``Issue 20575 runtime async ownership matrix`` optimized =
checkReraiseOwnership optimized "MATRIX"

[<Theory>]
[<InlineData(false)>]
[<InlineData(true)>]
let ``Issue 20575 legal synchronous exception region controls`` optimized =
checkReraiseOwnership optimized "CONTROLS"

let private compileInspectionProbe (body: string) =
// C# leaves these calls in their EH regions; these libraries must never be executed.
CSharp $"""
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

public static class Inspection
{{
public static void Probe(Task<int> audit)
{{
{body}
}}
}}
"""
|> withName "Inspection"
|> asLibrary
|> compile
|> shouldSucceed

[<Theory>]
[<InlineData("try { throw new Exception(); } catch { AsyncHelpers.Await((Task)audit); }", true)>]
[<InlineData("try { throw new Exception(); } catch { AsyncHelpers.Await(audit); }", true)>]
[<InlineData("try { throw new Exception(); } catch { AsyncHelpers.AwaitAwaiter(audit.GetAwaiter()); }", true)>]
[<InlineData("try { throw new Exception(); } catch { AsyncHelpers.UnsafeAwaitAwaiter(audit.GetAwaiter()); }", true)>]
[<InlineData("try { throw new Exception(); } finally { AsyncHelpers.Await(audit); }", true)>]
[<InlineData("try { throw new Exception(); } catch when (audit.IsCompleted) { AsyncHelpers.Await(audit); }", true)>]
[<InlineData("try { throw new Exception(); } catch when (AsyncHelpers.Await(audit) == 7) { }", true)>]
[<InlineData("try { AsyncHelpers.Await(audit); } catch { }", false)>]
let ``Issue 20575 inspection rejects suspension in exception regions`` body forbidden =
let result = compileInspectionProbe body
if forbidden then
let error =
Assert.Throws<System.Exception>(fun () ->
result |> verifyRuntimeAsyncExceptionRegions ["Inspection::Probe", false] |> ignore)
Assert.StartsWith("Inspection::Probe: suspension in exception handler/filter at IL_", error.Message)
Assert.Contains("regions (kind, try offset/length, handler offset/length, filter offset):", error.Message)
else
result |> verifyRuntimeAsyncExceptionRegions ["Inspection::Probe", false] |> ignore

[<Theory>]
[<InlineData("RuntimeAsyncTest::missing", false, "Missing probe method body: ")>]
[<InlineData("AbstractProbe::MissingBody", false, "Missing probe method body: ")>]
[<InlineData("RuntimeAsyncTest::rawBody", true, "Missing runtime-async body with suspension in ")>]
let ``Issue 20575 inspection rejects missing probes and suspension`` methodName requiresAwait message =
let result =
FSharp (runtimeAsyncSource + "\ntype AbstractProbe = abstract MissingBody: unit -> unit\n")
|> withLangVersionPreview
|> withFSharpCoreShippedNet
|> asLibrary
|> compile
|> shouldSucceed

let error =
Assert.Throws<System.Exception>(fun () ->
result |> verifyRuntimeAsyncExceptionRegions [methodName, requiresAwait] |> ignore)
Assert.Equal($"{message}{methodName}", error.Message)

[<Fact>]
let ``Issue 20575 inspection requires runtime async metadata even with suspension`` () =
let result = compileInspectionProbe "AsyncHelpers.Await(audit);"
result |> verifyRuntimeAsyncExceptionRegions ["Inspection::Probe", false] |> ignore
let error =
Assert.Throws<System.Exception>(fun () ->
result |> verifyRuntimeAsyncExceptionRegions ["Inspection::Probe", true] |> ignore)
Assert.Equal("Missing runtime-async body with suspension in Inspection::Probe", error.Message)

[<Fact>]
let ``runtime async rejects stackalloc across suspension`` () =
FSharp """
Expand Down
Loading
Loading