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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable changes to FScript are documented in this file.

## [Unreleased]

- Added a new `MagnusOpera.FScript.TypeProvider` package that type-checks scripts at compile time and exposes exported functions as strongly-typed F# members with runtime signature compatibility checks.

## [0.59.0]


Expand Down
30 changes: 30 additions & 0 deletions FScript.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FScript.LanguageServer", "s
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.Cli.Tests", "tests\FScript.Cli.Tests\FScript.Cli.Tests.fsproj", "{9B840598-3B03-457B-B1BE-9701BFD0D40A}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.TypeProvider", "src\FScript.TypeProvider\FScript.TypeProvider.fsproj", "{14D91D30-8E5E-482A-940B-CC55F2DE80AA}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.TypeProvider.Tests", "tests\FScript.TypeProvider.Tests\FScript.TypeProvider.Tests.fsproj", "{42D043DE-8987-4072-8841-DCB2144AC18C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -143,6 +147,30 @@ Global
{9B840598-3B03-457B-B1BE-9701BFD0D40A}.Release|x64.Build.0 = Release|Any CPU
{9B840598-3B03-457B-B1BE-9701BFD0D40A}.Release|x86.ActiveCfg = Release|Any CPU
{9B840598-3B03-457B-B1BE-9701BFD0D40A}.Release|x86.Build.0 = Release|Any CPU
{14D91D30-8E5E-482A-940B-CC55F2DE80AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{14D91D30-8E5E-482A-940B-CC55F2DE80AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{14D91D30-8E5E-482A-940B-CC55F2DE80AA}.Debug|x64.ActiveCfg = Debug|Any CPU
{14D91D30-8E5E-482A-940B-CC55F2DE80AA}.Debug|x64.Build.0 = Debug|Any CPU
{14D91D30-8E5E-482A-940B-CC55F2DE80AA}.Debug|x86.ActiveCfg = Debug|Any CPU
{14D91D30-8E5E-482A-940B-CC55F2DE80AA}.Debug|x86.Build.0 = Debug|Any CPU
{14D91D30-8E5E-482A-940B-CC55F2DE80AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{14D91D30-8E5E-482A-940B-CC55F2DE80AA}.Release|Any CPU.Build.0 = Release|Any CPU
{14D91D30-8E5E-482A-940B-CC55F2DE80AA}.Release|x64.ActiveCfg = Release|Any CPU
{14D91D30-8E5E-482A-940B-CC55F2DE80AA}.Release|x64.Build.0 = Release|Any CPU
{14D91D30-8E5E-482A-940B-CC55F2DE80AA}.Release|x86.ActiveCfg = Release|Any CPU
{14D91D30-8E5E-482A-940B-CC55F2DE80AA}.Release|x86.Build.0 = Release|Any CPU
{42D043DE-8987-4072-8841-DCB2144AC18C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42D043DE-8987-4072-8841-DCB2144AC18C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42D043DE-8987-4072-8841-DCB2144AC18C}.Debug|x64.ActiveCfg = Debug|Any CPU
{42D043DE-8987-4072-8841-DCB2144AC18C}.Debug|x64.Build.0 = Debug|Any CPU
{42D043DE-8987-4072-8841-DCB2144AC18C}.Debug|x86.ActiveCfg = Debug|Any CPU
{42D043DE-8987-4072-8841-DCB2144AC18C}.Debug|x86.Build.0 = Debug|Any CPU
{42D043DE-8987-4072-8841-DCB2144AC18C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42D043DE-8987-4072-8841-DCB2144AC18C}.Release|Any CPU.Build.0 = Release|Any CPU
{42D043DE-8987-4072-8841-DCB2144AC18C}.Release|x64.ActiveCfg = Release|Any CPU
{42D043DE-8987-4072-8841-DCB2144AC18C}.Release|x64.Build.0 = Release|Any CPU
{42D043DE-8987-4072-8841-DCB2144AC18C}.Release|x86.ActiveCfg = Release|Any CPU
{42D043DE-8987-4072-8841-DCB2144AC18C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -157,5 +185,7 @@ Global
{8A28B784-F90B-469C-91BE-F96F63ACEA32} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{57518676-01F0-4D5B-A53B-7A06DBA9AA04} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{9B840598-3B03-457B-B1BE-9701BFD0D40A} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{14D91D30-8E5E-482A-940B-CC55F2DE80AA} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{42D043DE-8987-4072-8841-DCB2144AC18C} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
15 changes: 15 additions & 0 deletions docs/architecture/assemblies-and-roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,20 @@ Responsibilities:
Use this when:
- You want C# ownership of the server host process while reusing existing language services.

### `FScript.TypeProvider`
Role:
- F# compile-time type provider for exported FScript functions.

Responsibilities:
- Parse and type-check `.fss` scripts during F# compilation.
- Project `[<export>]` functions as strongly-typed static members.
- Resolve compile-time/runtime extern providers.
- Enforce runtime signature compatibility using compile-time fingerprints.

Use this when:
- You want F# compile-time validation of script contracts.
- You want strongly-typed invocation of exported script functions without hand-written wrappers.

## Typical composition

### CLI execution path
Expand Down Expand Up @@ -109,6 +123,7 @@ Use this when:
- `FScript.Runtime` depends on `FScript.Language` types.
- `FScript.CSharpInterop` depends on both `FScript.Language` and `FScript.Runtime`.
- `FScript.LanguageServer` depends on `FScript.CSharpInterop`.
- `FScript.TypeProvider` depends on `FScript.Language` and `FScript.Runtime`.
- `FScript` depends on both `FScript.Language` and `FScript.Runtime`.

This keeps the language engine reusable while runtime capabilities remain host-configurable.
1 change: 1 addition & 0 deletions docs/specs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Normative behavior for the language, runtime surface, hosting model, and editor/
## Hosting and security

- Embedding `FScript.Language`: [`embedding-fscript-language.md`](./embedding-fscript-language.md)
- F# type provider for exported functions: [`fsharp-type-provider.md`](./fsharp-type-provider.md)
- Sandbox and security: [`sandbox-and-security.md`](./sandbox-and-security.md)

## Editor/LSP behavior
Expand Down
11 changes: 11 additions & 0 deletions docs/specs/embedding-fscript-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,14 @@ open FScript.Language
let typed = "[<export>] let run x = x" |> FScript.parse |> FScript.infer
let descriptors = Descriptor.describeFunctions typed Map.empty
```

### 6. Exported function signatures without evaluation
Use `FScript.Runtime.ExportSignatures.fromTypedProgram` when a host needs exported function signatures from typed AST without executing script bodies.

```fsharp
open FScript.Language
open FScript.Runtime

let typed = "[<export>] let add (x: int) (y: int) = x + y" |> FScript.parse |> FScript.infer
let signatures = ExportSignatures.fromTypedProgram typed
```
77 changes: 77 additions & 0 deletions docs/specs/fsharp-type-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# F# Type Provider for FScript Exports

This specification defines the contract for `MagnusOpera.FScript.TypeProvider`.

## Purpose

- Compile-time parse and type-check `.fss` scripts.
- Expose `[<export>]` functions as strongly-typed static methods in F#.
- Allow runtime script replacement with strict signature compatibility checks.

## Provider entry point

- Namespace: `FScript.TypeProvider`
- Type provider: `FScriptScriptProvider`

Static parameters:
- `ScriptPath: string` (required)
- `RootDirectory: string` (optional, defaults to script directory)
- `ExternProviders: string` (optional, semicolon-separated assembly-qualified provider type names)

## Compile-time behavior

1. Resolve script path and root directory.
2. Resolve externs using runtime defaults plus configured extern-provider types.
3. Parse with includes from file and run type inference.
4. Collect exported functions.
5. Fail compilation on:
- parse/type errors,
- unsupported exported signature shapes.

## Exposed members

For each exported function, generate one static method with mapped .NET/F# types.

Provider-generated static members:
- `SetRuntimeResolver : (unit -> RuntimeScriptOverride option) -> unit`
- `ClearRuntimeResolver : unit -> unit`

`RuntimeScriptOverride` fields:
- `RootDirectory: string`
- `EntryFile: string`
- `EntrySource: string`
- `ResolveImportedSource: (string -> string option) option`

## Supported exported signature mapping (v1)

Supported:
- `unit`
- `int` -> `int64`
- `float`
- `bool`
- `string`
- `list<T>`
- `option<T>`
- tuples (arity `2..8`)
- `map<string, T>` -> `Map<string, T>`

Rejected:
- records
- unions
- named/custom types
- function values in argument/return positions
- unresolved type variables
- non-string map keys

## Runtime compatibility policy

- Provider computes a compile-time fingerprint of all exported function signatures.
- Every invocation loads script via compile-time path or active runtime resolver override.
- Runtime exported signature fingerprint must exactly match compile-time fingerprint.
- Mismatch fails invocation with an error before function execution.

## Runtime load source selection

- No resolver set: load compile-time script file.
- Resolver set and returns `Some`: load `EntrySource` with `loadSourceWithIncludes` and optional import resolver.
- Resolver set and returns `None`: fallback to compile-time script file.
53 changes: 53 additions & 0 deletions src/FScript.Runtime/ExportSignatures.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace FScript.Runtime

open FScript.Language

module ExportSignatures =
type ExportedFunctionSignature =
{ Name: string
ParameterNames: string list
ParameterTypes: Type list
ReturnType: Type }

let private flattenFunctionType (t: Type) : Type list * Type =
let rec loop (acc: Type list) (current: Type) =
match current with
| TFun (arg, ret) -> loop (arg :: acc) ret
| _ -> List.rev acc, current
loop [] t

let private flattenParameterNames (expr: Expr) : string list =
let rec loop (acc: string list) (current: Expr) =
match current with
| ELambda (param, body, _) -> loop (param.Name :: acc) body
| _ -> List.rev acc
loop [] expr

let private fromLet (name: string) (expr: Expr) (exprType: Type) : ExportedFunctionSignature option =
let parameterNames = flattenParameterNames expr
let parameterTypes, returnType = flattenFunctionType exprType
if parameterNames.IsEmpty || parameterTypes.IsEmpty then
None
elif parameterNames.Length <> parameterTypes.Length then
raise (HostCommon.evalError $"Signature mismatch for function '{name}'")
else
Some
{ Name = name
ParameterNames = parameterNames
ParameterTypes = parameterTypes
ReturnType = returnType }

let fromTypedProgram (program: TypeInfer.TypedProgram) : Map<string, ExportedFunctionSignature> =
program
|> List.collect (function
| TypeInfer.TSLet(name, expr, exprType, _, isExported, _) when isExported ->
match fromLet name expr exprType with
| Some signature -> [ signature.Name, signature ]
| None -> []
| TypeInfer.TSLetRecGroup(bindings, isExported, _) when isExported ->
bindings
|> List.choose (fun (name, expr, exprType, _) ->
fromLet name expr exprType
|> Option.map (fun signature -> signature.Name, signature))
| _ -> [])
|> Map.ofList
1 change: 1 addition & 0 deletions src/FScript.Runtime/FScript.Runtime.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<Compile Include="JsonExterns.fs" />
<Compile Include="XmlExterns.fs" />
<Compile Include="Registry.fs" />
<Compile Include="ExportSignatures.fs" />
<Compile Include="ScriptHost.fs" />
</ItemGroup>

Expand Down
44 changes: 6 additions & 38 deletions src/FScript.Runtime/ScriptHost.fs
Original file line number Diff line number Diff line change
Expand Up @@ -36,46 +36,14 @@ module ScriptHost =
| TypeInfer.TSLetRecGroup(bindings, isExported, _) when isExported -> bindings |> List.map (fun (name, _, _, _) -> name)
| _ -> [])

let private flattenFunctionType (t: Type) : Type list * Type =
let rec loop (acc: Type list) (current: Type) =
match current with
| TFun (arg, ret) -> loop (arg :: acc) ret
| _ -> List.rev acc, current
loop [] t

let private flattenParameterNames (expr: Expr) : string list =
let rec loop (acc: string list) (current: Expr) =
match current with
| ELambda (param, body, _) -> loop (param.Name :: acc) body
| _ -> List.rev acc
loop [] expr

let private collectFunctionSignatures (program: TypeInfer.TypedProgram) : Map<string, FunctionSignature> =
let fromLet name expr exprType =
let paramNames = flattenParameterNames expr
let parameterTypes, returnType = flattenFunctionType exprType
if paramNames.IsEmpty || parameterTypes.IsEmpty then
None
elif paramNames.Length <> parameterTypes.Length then
raise (HostCommon.evalError $"Signature mismatch for function '{name}'")
else
Some (name,
{ Name = name
ParameterNames = paramNames
ParameterTypes = parameterTypes
ReturnType = returnType })

program
|> List.collect (function
| TypeInfer.TSLet(name, expr, exprType, _, isExported, _) when isExported ->
match fromLet name expr exprType with
| Some signature -> [ signature ]
| None -> []
| TypeInfer.TSLetRecGroup(bindings, isExported, _) when isExported ->
bindings
|> List.choose (fun (name, expr, exprType, _) -> fromLet name expr exprType)
| _ -> [])
|> Map.ofList
|> ExportSignatures.fromTypedProgram
|> Map.map (fun _ signature ->
{ Name = signature.Name
ParameterNames = signature.ParameterNames
ParameterTypes = signature.ParameterTypes
ReturnType = signature.ReturnType })

let private loadProgram (externs: ExternalFunction list) (program: Program) : LoadedScript =
let typed = FScript.inferWithExterns externs program
Expand Down
Loading