Add a JSON-RPC server mode to F# Interactive - #20396
Open
xperiandri wants to merge 3 commits into
Open
Conversation
Contributor
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
|
This comment has been minimized.
This comment has been minimized.
Contributor
|
An idea: we could initially make it an out of process part of VS, a fsi session wrapper alternative to fsmain. |
An editor hosting F# Interactive today drives it through standard input and recovers results by looking for a "SERVER-PROMPT>" marker in the output text. That protocol cannot say which output belongs to which submission and carries no structured diagnostics, which has pushed the Visual Studio window into a set of workarounds: a line directive wrapped around every selection, a temporary file the first submission writes its own process id into because under "dotnet fsi" the launched process is not the one evaluating code, counting output lines to skip what that discovery printed, and a separate remoting channel for interrupts. This adds "--fsi-server-jsonrpc:<pipe name>", in which a host submits interactions over a named pipe and receives results as data: diagnostics with positions, escaping exceptions, the evaluating process id, and the working directory. Program output keeps flowing through the redirected console streams, so the control channel and the script's own output no longer have to be told apart after the fact, and the prompt is suppressed rather than filtered back out. The transport is StreamJsonRpc over a HeaderDelimitedMessageHandler, the same combination Roslyn's interactive host uses. The protocol types live in one file so that a host can share the source rather than restate them. Threading is unchanged in the way that matters: interactions are evaluated on the event loop thread through EventLoopInvoke, exactly as the standard input path already does, so scripts that create user interface objects behave as they do at the console. Interactions queue onto a single worker and complete their tasks there, which leaves the library free to dispatch an interrupt while one is still running - an interrupt that waited its turn behind the interaction it is meant to stop would never arrive. The changes to fsi.fs are five hunks, all guarded by the new option and with no public API change: register the option, treat the mode as a server mode so the console reader is not used, suppress the prompt, skip the standard input reader thread, and keep the legacy interrupt channel for the legacy server mode only. The session watches the process id it was given and exits when it goes, so a crashed editor does not leave an orphan behind. Tested by 23 tests that launch a real fsi process and drive the protocol over the pipe; the parts most likely to break only exist across a process boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NoBloat asks that comments answer a "why" no name can express, and keeps rationale for design choices in the commit message rather than in the code. Comments only; no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
xperiandri
force-pushed
the
fsi-json-rpc
branch
from
August 31, 2026 23:05
02f94fd to
fde33b0
Compare
Contributor
|
🔍 Tooling Safety Check — Affects-Build-Infra, Affects-Restore, Affects-Test-Tooling
|
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.
Description
Adds a JSON-RPC server mode to F# Interactive:
--fsi-server-jsonrpc:<pipe name>.This is the first stage of replacing the Visual Studio F# Interactive window. It changes only
F# Interactive, is useful to any editor on its own, and carries no dependency on the window work
that will build on it. That work — a window hosted on
Microsoft.VisualStudio.InteractiveWindow,the same REPL engine C# Interactive and Python Interactive run on — will come as a separate PR.
Why
An editor hosting F# Interactive today drives it through standard input and recovers results by
looking for a
SERVER-PROMPT>marker in the output text. That protocol cannot say which outputbelongs to which submission, carries no structured diagnostics, and has pushed the Visual Studio
window into a set of workarounds that are worth listing, because each one disappears here:
dotnet fsithe process the IDE launched is not the process evaluating code — and the IDE thenpolls for that file in a
Thread.Sleeploop;val it: unit = ()that the process-id discovery itselfproduced;
CtrlBreakClient) carrying interrupts alongside the text protocol.What the mode does
The host submits interactions over a named pipe and receives results as data. Program output keeps
flowing through the redirected console streams, so the control channel and the script's own output
no longer have to be told apart after the fact. The prompt is suppressed in this mode, so nothing
has to be filtered back out of the output either.
fsi/initializefsi/executefsi/executeFilefsi/setPathsfsi/interruptfsi/shutdownThree details are worth calling out:
processIdis the process evaluating code. Underdotnet fsithat is not the process thehost launched, and it is what a debugger has to attach to. Reporting it in the handshake is what
removes the temporary-file dance.
sourcePathandstartLinemake the session emit the line directive itself, so a selectionexecuted from an editor reports its errors against the user's own file and line.
exceptionis omitted when the interaction merely failed to compile. The diagnostics alreadydescribe that, and a host reporting both would say the same thing twice.
Transport
StreamJsonRpc over a
HeaderDelimitedMessageHandler, the same combination Roslyn's interactive hostuses. The protocol types live in one file compiled into fsi, so a host can share the source rather
than restate them. The version is pinned to the one Roslyn's packages already force into
vsintegration, so the two agree.This adds eight assemblies (~3 MB) next to
fsi.dll: StreamJsonRpc, Newtonsoft.Json, MessagePackand its annotations, Nerdbank.MessagePack, Nerdbank.Streams, Microsoft.VisualStudio.Threading and
Microsoft.VisualStudio.Validation. Worth naming explicitly, since fsi ships inside the SDK. The
alternative was framing and dispatch maintained here, against a library that is already a dependency
of the editor on the other end of the same connection.
Threading
Unchanged from a console session in the way that matters: interactions are evaluated on the event
loop thread through
EventLoopInvoke, exactly as the standard input path already does, so scriptsthat create user interface objects behave as they do at the console.
Interactions queue onto a single worker so that they run in the order they arrived, and complete
their tasks there. That leaves StreamJsonRpc free to dispatch
fsi/interruptwhile an interactionis still running — an interrupt that waited its turn behind the interaction it is meant to stop
would never arrive.
Changes to
src/Compiler/Interactive/fsi.fsFive small hunks, all guarded by the new option, no public API change:
--fsi-server-jsonrpc;request;
Lifetime
The session watches the process id it was given and exits when it goes, so a crashed editor does not
leave an orphan behind. Closing the control channel has the same effect.
Tests
tests/FSharp.Compiler.Interactive.Server.Tests— 23 tests that launch a real fsi process and drivethe protocol over the pipe. The parts most likely to break are the ones that only exist across a
process boundary, so an in-process stand-in would not have exercised them: the handshake, the
lifetime of the session, and the interaction between the control channel and the output streams.
Covered: state kept across interactions, structured diagnostics with error numbers and positions,
diagnostics attributed to the host's own file and line, escaping exceptions, warnings not failing an
interaction, text that has to survive JSON escaping,
#load, working-directory changes, interruptinga running interaction, refusing requests before the handshake, refusing unknown methods, and exiting
when the host disconnects or its process dies.
Checklist