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
5 changes: 5 additions & 0 deletions .github/workflows/ci-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ on:
pull_request:
branches:
- main
types:
- opened
- reopened
- synchronize
- ready_for_review

permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/on-push-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
shell: bash
run: |
rm -rf vscode-fscript/server
dotnet publish src/FScript.LanguageServer/FScript.LanguageServer.fsproj -c Release -p:PublishAot=false -o vscode-fscript/server
dotnet publish src/FScript.LanguageServer/FScript.LanguageServer.csproj -c Release -p:PublishAot=false -o vscode-fscript/server

- name: Install extension packaging tool
run: npm install -g @vscode/vsce@3.5
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@ All notable changes to FScript are documented in this file.

## [Unreleased]

- Removed F# sources from `src/FScript.LanguageServer*` by moving LSP semantic modules into `FScript.CSharpInterop` and keeping `FScript.LanguageServer` as C# host.
- Replaced `FScript.LanguageServer.Tests` project with a C# test project and C# LSP test harness to remove F# compile cost from LanguageServer test builds.
- Deleted obsolete F# LanguageServer test sources after C# test project migration.
- Renamed `FScript.CSharpInterop/LanguageServerLegacy` to `FScript.CSharpInterop/LanguageServer` to reflect the new primary architecture.
- CI now runs branch update builds on PR `synchronize` events while keeping `ci-main` scoped to `main` pushes to avoid duplicate runs.
- Enabled F# preview parallel compilation globally, disabled deterministic builds, and removed global RuntimeIdentifiers to reduce CI build latency.
- Added `FScript.CSharpInterop` as a stable bridge for parse/infer/runtime-extern/stdlib-source services and wired LanguageServer through it.
- Added `FScript.LanguageServer` host executable as the migration entrypoint for C#-owned LSP startup.
- Added a first native C# LSP server core (JSON-RPC transport, initialize/shutdown, text sync, and stdlib-source request) with dedicated integration tests.
- Extended the native C# LSP core with diagnostics publishing and `viewAst`/`viewInferredAst` command handling.
- Switched C# LSP host to full-method dispatch parity via shared handlers, made it the default test target, and updated extension/tag packaging to use `FScript.LanguageServer.dll`.
- Replaced the F# LSP server executable with `FScript.LanguageServer` (C#) and moved F# LSP logic into `FScript.LanguageServer.Core`.
- Fixed imported qualified type annotations (for example `common.ProjectInfo`) in parser/type inference to prevent false type mismatches.

## [0.33.0]

Expand Down
45 changes: 30 additions & 15 deletions FScript.sln
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.Runtime", "src\FScr
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.Runtime.Tests", "tests\FScript.Runtime.Tests\FScript.Runtime.Tests.fsproj", "{1E2C7B34-04B8-42C9-880D-CC47DEC156A7}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.LanguageServer", "src\FScript.LanguageServer\FScript.LanguageServer.fsproj", "{E22A34B5-F5E8-422D-9BA5-932B3C45188F}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FScript.LanguageServer.Tests", "tests\FScript.LanguageServer.Tests\FScript.LanguageServer.Tests.csproj", "{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.LanguageServer.Tests", "tests\FScript.LanguageServer.Tests\FScript.LanguageServer.Tests.fsproj", "{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}"
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.CSharpInterop", "src\FScript.CSharpInterop\FScript.CSharpInterop.fsproj", "{8A28B784-F90B-469C-91BE-F96F63ACEA32}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FScript.LanguageServer", "src\FScript.LanguageServer\FScript.LanguageServer.csproj", "{57518676-01F0-4D5B-A53B-7A06DBA9AA04}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -91,18 +93,6 @@ Global
{1E2C7B34-04B8-42C9-880D-CC47DEC156A7}.Release|x64.Build.0 = Release|Any CPU
{1E2C7B34-04B8-42C9-880D-CC47DEC156A7}.Release|x86.ActiveCfg = Release|Any CPU
{1E2C7B34-04B8-42C9-880D-CC47DEC156A7}.Release|x86.Build.0 = Release|Any CPU
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|x64.ActiveCfg = Debug|Any CPU
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|x64.Build.0 = Debug|Any CPU
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|x86.ActiveCfg = Debug|Any CPU
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|x86.Build.0 = Debug|Any CPU
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|Any CPU.Build.0 = Release|Any CPU
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|x64.ActiveCfg = Release|Any CPU
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|x64.Build.0 = Release|Any CPU
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|x86.ActiveCfg = Release|Any CPU
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|x86.Build.0 = Release|Any CPU
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Debug|x64.ActiveCfg = Debug|Any CPU
Expand All @@ -115,6 +105,30 @@ Global
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Release|x64.Build.0 = Release|Any CPU
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Release|x86.ActiveCfg = Release|Any CPU
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Release|x86.Build.0 = Release|Any CPU
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|x64.ActiveCfg = Debug|Any CPU
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|x64.Build.0 = Debug|Any CPU
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|x86.ActiveCfg = Debug|Any CPU
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|x86.Build.0 = Debug|Any CPU
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|Any CPU.Build.0 = Release|Any CPU
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|x64.ActiveCfg = Release|Any CPU
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|x64.Build.0 = Release|Any CPU
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|x86.ActiveCfg = Release|Any CPU
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|x86.Build.0 = Release|Any CPU
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|Any CPU.Build.0 = Debug|Any CPU
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|x64.ActiveCfg = Debug|Any CPU
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|x64.Build.0 = Debug|Any CPU
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|x86.ActiveCfg = Debug|Any CPU
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|x86.Build.0 = Debug|Any CPU
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|Any CPU.ActiveCfg = Release|Any CPU
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|Any CPU.Build.0 = Release|Any CPU
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|x64.ActiveCfg = Release|Any CPU
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|x64.Build.0 = Release|Any CPU
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|x86.ActiveCfg = Release|Any CPU
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -125,7 +139,8 @@ Global
{9C62883E-EFB0-4D9E-84F3-4138C123F55E} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{8C2C5767-857A-44B0-80C2-DC90E0A60F4D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{1E2C7B34-04B8-42C9-880D-CC47DEC156A7} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{E22A34B5-F5E8-422D-9BA5-932B3C45188F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{8A28B784-F90B-469C-91BE-F96F63ACEA32} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{57518676-01F0-4D5B-A53B-7A06DBA9AA04} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal
28 changes: 28 additions & 0 deletions docs/architecture/assemblies-and-roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,32 @@ Use this when:
NuGet:
- `MagnusOpera.FScript.Runtime`

### `FScript.CSharpInterop`
Role:
- C#-friendly integration facade over language + runtime services.

Responsibilities:
- Resolve runtime extern catalog from source path/root context.
- Parse with include/import expansion through a stable interop entry point.
- Run inference APIs through a single host-facing surface.
- Expose stdlib virtual source loading for editor integrations.

Use this when:
- You integrate FScript from C# and want to avoid direct F# compiler/runtime internals.
- You build tooling services (for example LSP hosts) with a stable boundary.

### `FScript.LanguageServer`
Role:
- C# host executable for the Language Server process.

Responsibilities:
- Provide the production C# process host for LSP startup/dispatch.
- Execute the full LSP method surface used by the VS Code extension.
- Keep protocol behavior aligned with existing language/runtime analysis services.

Use this when:
- You want C# ownership of the server host process while reusing existing language services.

## Typical composition

### CLI execution path
Expand All @@ -72,6 +98,8 @@ NuGet:
## Dependency direction
- `FScript.Language` has no dependency on `FScript.Runtime`.
- `FScript.Runtime` depends on `FScript.Language` types.
- `FScript.CSharpInterop` depends on both `FScript.Language` and `FScript.Runtime`.
- `FScript.LanguageServer` depends on `FScript.CSharpInterop`.
- `FScript` depends on both `FScript.Language` and `FScript.Runtime`.

This keeps the language engine reusable while runtime capabilities remain host-configurable.
23 changes: 23 additions & 0 deletions src/FScript.CSharpInterop/FScript.CSharpInterop.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<Compile Include="InteropServices.fs" />
<Compile Include="LanguageServer\LspProtocol.fs" />
<Compile Include="LanguageServer\LspModel.fs" />
<Compile Include="LanguageServer\LspRuntimeExterns.fs" />
<Compile Include="LanguageServer\AstJson.fs" />
<Compile Include="LanguageServer\LspSymbols.fs" />
<Compile Include="LanguageServer\LspHandlers.fs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\FScript.Language\FScript.Language.fsproj" />
<ProjectReference Include="..\FScript.Runtime\FScript.Runtime.fsproj" />
</ItemGroup>

</Project>
63 changes: 63 additions & 0 deletions src/FScript.CSharpInterop/InteropServices.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
namespace FScript.CSharpInterop

open System
open System.IO
open FScript.Language
open FScript.Runtime

module InteropServices =
let private resolveRootDirectory (sourcePath: string) =
try
match Path.GetDirectoryName(sourcePath) with
| null
| "" -> Directory.GetCurrentDirectory()
| dir -> dir
with _ ->
Directory.GetCurrentDirectory()

let runtimeExternsForSourcePath (sourcePath: string) : ExternalFunction list =
let ctx = { HostContext.RootDirectory = resolveRootDirectory sourcePath }
Registry.all ctx

let parseProgramFromSourceWithIncludes (sourcePath: string) (sourceText: string) : Program =
let rootDirectory = resolveRootDirectory sourcePath
IncludeResolver.parseProgramFromSourceWithIncludes rootDirectory sourcePath sourceText

let inferProgramWithExterns (externs: ExternalFunction list) (program: Program) : TypeInfer.TypedProgram =
TypeInfer.inferProgramWithExterns externs program

let inferProgramWithExternsAndLocalVariableTypes (externs: ExternalFunction list) (program: Program) : TypeInfer.TypedProgram * TypeInfer.LocalVariableTypeInfo list =
TypeInfer.inferProgramWithExternsAndLocalVariableTypes externs program

let inferStdlibWithExternsRaw (externs: ExternalFunction list) : TypeInfer.TypedProgram =
TypeInfer.inferProgramWithExternsRaw externs (Stdlib.loadProgram())

let stdlibProgram () : Program =
Stdlib.loadProgram()

let tryLoadStdlibSourceText (uri: string) : string option =
try
let parsed = Uri(uri)
if not (String.Equals(parsed.Scheme, "fscript-stdlib", StringComparison.OrdinalIgnoreCase)) then
None
else
let fileName = parsed.AbsolutePath.TrimStart('/')
let resourceName =
match fileName with
| "Option.fss" -> Some "FScript.Language.Stdlib.Option.fss"
| "List.fss" -> Some "FScript.Language.Stdlib.List.fss"
| "Map.fss" -> Some "FScript.Language.Stdlib.Map.fss"
| _ -> None

match resourceName with
| None -> None
| Some name ->
let assembly = typeof<Span>.Assembly
match assembly.GetManifestResourceStream(name) with
| null -> None
| stream ->
use stream = stream
use reader = new StreamReader(stream)
Some(reader.ReadToEnd())
with _ ->
None
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ open System
open System.IO
open System.Text.Json.Nodes
open FScript.Language
open FScript.CSharpInterop

module LspHandlers =
open LspModel
Expand Down Expand Up @@ -497,12 +498,7 @@ module LspHandlers =
| None ->
sendCommandError idNode "internal" $"Unable to read source file '{sourcePath}'."
| Some sourceText ->
let rootDirectory =
match Path.GetDirectoryName(sourcePath) with
| null
| "" -> "."
| dir -> dir
let program = IncludeResolver.parseProgramFromSourceWithIncludes rootDirectory sourcePath sourceText
let program = InteropServices.parseProgramFromSourceWithIncludes sourcePath sourceText
let response = JsonObject()
response["ok"] <- JsonValue.Create(true)
response["data"] <- AstJson.programToJson sourcePath program
Expand All @@ -528,14 +524,9 @@ module LspHandlers =
| None ->
sendCommandError idNode "internal" $"Unable to read source file '{sourcePath}'."
| Some sourceText ->
let rootDirectory =
match Path.GetDirectoryName(sourcePath) with
| null
| "" -> "."
| dir -> dir
let program = IncludeResolver.parseProgramFromSourceWithIncludes rootDirectory sourcePath sourceText
let program = InteropServices.parseProgramFromSourceWithIncludes sourcePath sourceText
let runtimeExterns = LspRuntimeExterns.forSourcePath sourcePath
let typedProgram = TypeInfer.inferProgramWithExterns runtimeExterns program
let typedProgram = InteropServices.inferProgramWithExterns runtimeExterns program
let response = JsonObject()
response["ok"] <- JsonValue.Create(true)
response["data"] <- AstJson.typedProgramToJson sourcePath typedProgram
Expand Down Expand Up @@ -1103,40 +1094,12 @@ module LspHandlers =
| _ ->
LspProtocol.sendResponse idNode None

let private tryLoadStdlibSourceText (uri: string) =
try
let parsed = Uri(uri)
if not (String.Equals(parsed.Scheme, "fscript-stdlib", StringComparison.OrdinalIgnoreCase)) then
None
else
let fileName = parsed.AbsolutePath.TrimStart('/')
let resourceName =
match fileName with
| "Option.fss" -> Some "FScript.Language.Stdlib.Option.fss"
| "List.fss" -> Some "FScript.Language.Stdlib.List.fss"
| "Map.fss" -> Some "FScript.Language.Stdlib.Map.fss"
| _ -> None

match resourceName with
| None -> None
| Some name ->
let assembly = typeof<Span>.Assembly
match assembly.GetManifestResourceStream(name) with
| null ->
None
| stream ->
use stream = stream
use reader = new StreamReader(stream)
Some (reader.ReadToEnd())
with _ ->
None

let handleStdlibSource (idNode: JsonNode) (paramsObj: JsonObject) =
match tryGetString paramsObj "uri" with
| None ->
sendCommandError idNode "internal" "Missing stdlib URI."
| Some uri ->
match tryLoadStdlibSourceText uri with
match InteropServices.tryLoadStdlibSourceText uri with
| Some sourceText ->
let response = JsonObject()
response["ok"] <- JsonValue.Create(true)
Expand Down
8 changes: 8 additions & 0 deletions src/FScript.CSharpInterop/LanguageServer/LspRuntimeExterns.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace FScript.LanguageServer

open FScript.Language
open FScript.CSharpInterop

module LspRuntimeExterns =
let forSourcePath (sourcePath: string) : ExternalFunction list =
InteropServices.runtimeExternsForSourcePath sourcePath
Loading