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
13 changes: 13 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"fable": {
"version": "5.2.0",
"commands": [
"fable"
],
"rollForward": false
}
}
}
3 changes: 3 additions & 0 deletions .github/workflows/ci-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ jobs:
cache: npm
cache-dependency-path: website/package-lock.json

- name: Run Fable JS tests
run: make fable-test

- name: Build website
run: |
cd website
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:
cache: npm
cache-dependency-path: website/package-lock.json

- name: Run Fable JS tests
run: make fable-test

- name: Build website
run: |
cd website
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ Thumbs.db

vscode-fscript/dist/
vscode-fscript/*.vsix

src/FScript.JavaScript/dist/
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to FScript are documented in this file.

## [Unreleased]

- Added a Fable-compiled JavaScript embedding package for running FScript in Node and browser-compatible ESM hosts.
- Added website documentation and a browser sandbox for the Fable JavaScript FScript runtime.

## [0.73.0]


Expand Down
15 changes: 15 additions & 0 deletions FScript.sln
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.TypeProvider", "src
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.TypeProvider.Tests", "tests\FScript.TypeProvider.Tests\FScript.TypeProvider.Tests.fsproj", "{42D043DE-8987-4072-8841-DCB2144AC18C}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.JavaScript", "src\FScript.JavaScript\FScript.JavaScript.fsproj", "{CEB16446-D96F-40A7-9771-F90B9A78D7B5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -171,6 +173,18 @@ Global
{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
{CEB16446-D96F-40A7-9771-F90B9A78D7B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEB16446-D96F-40A7-9771-F90B9A78D7B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEB16446-D96F-40A7-9771-F90B9A78D7B5}.Debug|x64.ActiveCfg = Debug|Any CPU
{CEB16446-D96F-40A7-9771-F90B9A78D7B5}.Debug|x64.Build.0 = Debug|Any CPU
{CEB16446-D96F-40A7-9771-F90B9A78D7B5}.Debug|x86.ActiveCfg = Debug|Any CPU
{CEB16446-D96F-40A7-9771-F90B9A78D7B5}.Debug|x86.Build.0 = Debug|Any CPU
{CEB16446-D96F-40A7-9771-F90B9A78D7B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEB16446-D96F-40A7-9771-F90B9A78D7B5}.Release|Any CPU.Build.0 = Release|Any CPU
{CEB16446-D96F-40A7-9771-F90B9A78D7B5}.Release|x64.ActiveCfg = Release|Any CPU
{CEB16446-D96F-40A7-9771-F90B9A78D7B5}.Release|x64.Build.0 = Release|Any CPU
{CEB16446-D96F-40A7-9771-F90B9A78D7B5}.Release|x86.ActiveCfg = Release|Any CPU
{CEB16446-D96F-40A7-9771-F90B9A78D7B5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -187,5 +201,6 @@ Global
{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}
{CEB16446-D96F-40A7-9771-F90B9A78D7B5} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
.PHONY: build test smoke-tests verify-changelog release-prepare clean publish publish-darwin publish-linux publish-windows pack-nuget publish-all website website-install website-build website-version website-typecheck
.PHONY: build test fable-build fable-test smoke-tests verify-changelog release-prepare clean publish publish-darwin publish-linux publish-windows pack-nuget publish-all website website-install website-build website-version website-typecheck

build:
dotnet build FScript.sln -c Release

test:
dotnet test FScript.sln -c Release

fable-build:
dotnet tool restore
dotnet fable src/FScript.JavaScript/FScript.JavaScript.fsproj --outDir src/FScript.JavaScript/dist --sourceMaps false

fable-test: fable-build
node --test src/FScript.JavaScript/test/*.test.mjs

smoke-tests:
@set -e; \
for script in samples/*.fss; do \
Expand Down
34 changes: 33 additions & 1 deletion docs/architecture/assemblies-and-roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,30 @@ Use this when:
NuGet:
- `MagnusOpera.FScript.Language`

Fable:
- The project is Fable-compatible for source package consumers.
- Fable hosts should use the JavaScript facade rather than consuming raw F# discriminated unions directly.

### `FScript.JavaScript`
Role:
- Fable-compiled JavaScript facade for Node and browser-compatible ESM hosts.

Responsibilities:
- Compile `FScript.Language` to JavaScript through Fable.
- Expose stable JS entry points such as `run`, `parse`, `infer`, `evaluate`, `load`, and `invoke`.
- Provide a small stateful session API for browser sandbox and REPL-like JavaScript hosts.
- Convert between FScript runtime values and tagged JavaScript objects.
- Accept JavaScript-provided externs with explicit type schemes.
- Resolve imports through host-provided virtual source maps or resolver callbacks.

Use this when:
- You want to run or embed FScript from JavaScript.
- You need repeated invocation of exported script functions from a JS host.
- You want to provide JS externs without loading the .NET runtime extern catalog.

npm:
- `@magnusopera/fscript`

### `FScript.Runtime`
Role:
- Runtime integration layer and extern catalog.
Expand Down Expand Up @@ -118,12 +142,20 @@ Use this when:
3. Host runs parse/infer/eval programmatically.
4. Host decides output, logging, and error handling behavior.

### JavaScript host path
1. Host app imports `@magnusopera/fscript` as an ESM package.
2. Host provides optional JS externs through `extern({ name, arity, scheme, invoke })`.
3. Host executes `run` for one-shot evaluation or `load` plus `invoke` for repeated calls.
4. Host receives tagged JS values and structured JS errors.
5. Host resolves imports from virtual paths using `sources` or `resolveImport`; no file-backed resolver is available in Fable builds.

## Dependency direction
- `FScript.Language` has no dependency on `FScript.Runtime`.
- `FScript.JavaScript` depends on `FScript.Language` and Fable packages.
- `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.
This keeps the language engine reusable while runtime capabilities remain host-configurable. The JavaScript facade is intentionally separate from `.NET` runtime externs: v1 exposes core language hosting, virtual imports, tagged values, and JS-provided externs, while filesystem, console, task, and other runtime modules stay in the .NET runtime layer.
101 changes: 101 additions & 0 deletions docs/specs/embedding-fscript-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,28 @@ Primary entry points are in module `FScript`:
- `FScript.evalWithExterns : ExternalFunction list -> TypeInfer.TypedProgram -> Value`
- `FScript.run : string -> Value` (parse + infer + eval without externs)

JavaScript hosts should use the Fable-compiled package surface instead of raw F# discriminated unions. The ESM package is published as `@magnusopera/fscript` and exports:

- `run(source, options?)`
- `parse(source, options?)`
- `infer(program, externs?)`
- `evaluate(typedProgram, externs?)`
- `load(source, options?)`
- `invoke(loaded, name, args)`
- `listFunctions(loaded)`
- `listValues(loaded)`
- `getValue(loaded, name)`
- `formatValue(value)`
- `toJs(value)`
- `fromJs(value, expectedType?)`
- `createSession(options?)`
- `submit(session, source)`
- `resetSession(session)`
- `T` type and scheme builders
- `extern({ name, arity, scheme, invoke })`

The JavaScript package targets Node and browser-compatible ESM hosts. It embeds `FScript.Language` through Fable and does not include the .NET `FScript.Runtime` extern catalog.

## Running scripts

```fsharp
Expand Down Expand Up @@ -60,6 +82,85 @@ let typed = FScript.inferWithExterns [ toUpperExtern ] program
let result = FScript.evalWithExterns [ toUpperExtern ] typed
```

## JavaScript embedding

Use `run` for one-shot execution and `load` when the host will invoke exported functions repeatedly.

```javascript
import { T, extern, run, load, invoke, getValue } from "@magnusopera/fscript";

const shout = extern({
name: "Host.shout",
arity: 1,
scheme: T.scheme(T.func(T.string, T.string)),
invoke(args) {
return { kind: "string", value: `${args[0].value}!` };
}
});

const value = run("Host.shout \"fable\"", { externs: [shout] });
// value = { kind: "string", value: "fable!" }

const loaded = load("[<export>] let add x y = x + y\n[<export>] let answer = 42");
const sum = invoke(loaded, "add", [1, 2]);
const answer = getValue(loaded, "answer");
```

JavaScript values crossing the host boundary use tagged objects:

- `{ kind: "unit" }`
- `{ kind: "int", value: 42n }`
- `{ kind: "float", value: 3.14 }`
- `{ kind: "bool", value: true }`
- `{ kind: "string", value: "text" }`
- `{ kind: "list", values: [...] }`
- `{ kind: "tuple", values: [...] }`
- `{ kind: "record", fields: { ... } }`
- `{ kind: "map", entries: [{ key, value }] }`
- `{ kind: "option", value: null | taggedValue }`
- `{ kind: "union", typeName, caseName, value: null | taggedValue }`
- `{ kind: "type", name }`
- `{ kind: "opaque", valueType }`

`int` values are emitted as JavaScript `bigint`. Inputs may use `bigint`, safe integer `number`, string, or tagged `{ kind: "int", value }` forms. Functions, externals, tasks, and union constructors are opaque values; exported functions are invoked through `invoke`.

JavaScript errors thrown by the facade have `kind: "fscript-error"`, `phase` (`parse`, `type`, `eval`, or `host`), `message`, and span data when the underlying language stage provides it.

### JavaScript sessions

Browser sandbox and REPL-like hosts can use the stateful session API:

```javascript
const session = createSession({
rootDirectory: "/",
entryFile: "/main.fss"
});

submit(session, "let add x y = x + y");
const result = submit(session, "add 20 22");
// result = { kind: "session-result", hasValue: true, value, text: "42", retainedCount: 1 }

resetSession(session);
```

Sessions retain submitted declarations and type declarations. Expression-only submissions are evaluated against retained declarations and return a formatted `text` result plus the tagged `value`. The browser session API mirrors the CLI REPL's core state model, but it does not include console-specific controls such as prompts, EOF handling, or default .NET runtime externs.

### JavaScript imports

Fable builds do not use the .NET file-backed import resolver. A JavaScript host must supply imports through virtual paths:

```javascript
const loaded = load("import \"shared.fss\" as Shared\n[<export>] let value = Shared.inc 41", {
rootDirectory: "/",
entryFile: "/main.fss",
sources: {
"/shared.fss": "let inc x = x + 1"
}
});
```

The facade also accepts `resolveImport(path)` when imports should be loaded lazily. In JavaScript, imports are confined to the virtual `rootDirectory`, must resolve to `.fss` paths, and must return source text from the host-provided source map or callback. File system reads and symlink checks remain .NET-only behavior.

## Loading once and invoking by name

Use `FScript.Runtime.ScriptHost` when a host needs reusable loading and direct function invocation.
Expand Down
23 changes: 23 additions & 0 deletions src/FScript.JavaScript/FScript.JavaScript.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>false</IsPackable>
<Nullable>disable</Nullable>
<NoWarn>$(NoWarn);FS0988</NoWarn>
</PropertyGroup>

<ItemGroup>
<Compile Include="Library.fs" />
</ItemGroup>

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

<ItemGroup>
<PackageReference Include="Fable.Core" Version="5.0.0" />
</ItemGroup>

</Project>
Loading
Loading