Skip to content
Open
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
All notable changes to FScript are documented in this file.

## [Unreleased]
- Added native `Task.spawn` and `Task.await` support with `'a task` types for concurrent thunk execution.

## [0.67.1]

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ Useful samples:
- [`samples/types-showcase.fss`](samples/types-showcase.fss)
- [`samples/patterns-and-collections.fss`](samples/patterns-and-collections.fss)
- [`samples/quicksort.fss`](samples/quicksort.fss)
- [`samples/parallel-quicksort.fss`](samples/parallel-quicksort.fss)
- [`samples/tree.fss`](samples/tree.fss)
- [`samples/mutual-recursion.fss`](samples/mutual-recursion.fss)
- [`samples/imports-and-exports.fss`](samples/imports-and-exports.fss)
Expand All @@ -168,7 +169,7 @@ Each extern declares:
- implementation.

Built-in host extern families include `Fs.*`, `Json.*`, `Xml.*`, `Regex.*`, hashing, GUIDs, and `print`.
`List.*`, `Map.*`, and `Option.*` are provided by the embedded stdlib prelude.
`Task.*`, `List.*`, `Map.*`, and `Option.*` are provided by the embedded stdlib prelude.

For details and extension workflow, see [`docs/specs/external-functions.md`](docs/specs/external-functions.md).

Expand Down
11 changes: 11 additions & 0 deletions docs/guides/getting-started-tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ let rec fib n =
FScript ships with a preloaded stdlib focused on functional collection workflows.

Common families:
- `Task.*`
- `List.*`
- `Option.*`
- `Map.*`
Expand All @@ -377,6 +378,16 @@ let m = { ["a"] = 1; ["b"] = 2 }
let hasA = m |> Map.containsKey "a"
```

Concurrent task example:

```fsharp
let pending = Task.spawn (fun _ -> 40 + 2)
let answer = Task.await pending
print $"{answer}"
```

Tasks use the native type form `'a task`. Spawned thunks run concurrently, `Task.await` synchronizes on the result, and task failures remain fatal runtime errors.

Full reference:
- [`docs/specs/stdlib-functions.md`](../specs/stdlib-functions.md)

Expand Down
4 changes: 4 additions & 0 deletions docs/specs/sandbox-and-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ This document defines the security model for running FScript programs with host
- FScript executes in-process inside the host .NET application.
- Language-level side effects occur through registered externs.
- Host configuration determines the effective capability surface.
- `Task.spawn` runs FScript thunks concurrently on the host runtime thread pool.
- Concurrent task side effects may interleave with the main script and with other tasks.

## Host context and filesystem boundary
- Host context includes:
Expand Down Expand Up @@ -35,6 +37,8 @@ Filesystem extern behavior:
- Extern invocation checks arity and argument type-shape.
- Data/IO externs frequently model operational failures as `None` values.
- Script type misuse raises `TypeException`/`EvalException`.
- Task failures are fatal runtime errors, consistent with foreground evaluation.
- Programs that finish with unawaited tasks fail at runtime instead of silently detaching background work.

## Resource-governance model
- Evaluator execution currently relies on host/runtime process limits.
Expand Down
15 changes: 15 additions & 0 deletions docs/specs/stdlib-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This document lists the functions available in the embedded FScript prelude (std
The stdlib is loaded automatically by `FScript.Language` before user scripts.

## Modules
- `Task`
- `List`
- `Option`
- `Map`
Expand Down Expand Up @@ -55,6 +56,16 @@ The stdlib is loaded automatically by `FScript.Language` before user scripts.
- `Some : 'a -> 'a option`
- `None : 'a option`

### Tasks
- Module functions:
- `Task.spawn : (unit -> 'a) -> 'a task`
- `Task.await : 'a task -> 'a`
- Description:
- `Task.spawn` schedules a thunk for concurrent execution and returns an opaque task handle immediately
- `Task.await` waits for completion and returns the result value
- task failures are fatal runtime errors
- ending a program with unawaited tasks is a runtime error

### Records
- Field access: `record.Field`
- Signature: `{ Field: 'a; ... } -> 'a`
Expand Down Expand Up @@ -91,6 +102,10 @@ The stdlib is loaded automatically by `FScript.Language` before user scripts.
- `Option.isSome : 'a option -> bool`
- `Option.map : ('a -> 'b) -> 'a option -> 'b option`

## Task
- `Task.spawn : (unit -> 'a) -> 'a task`
- `Task.await : 'a task -> 'a`

## Map
- `Map.empty : 'v map`
Alias of `{}`.
Expand Down
12 changes: 12 additions & 0 deletions docs/specs/supported-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This document specifies the value and type system used by the interpreter.

## Composite/container types
- List: `'a list`
- Task: `'a task`
- Tuple: `(t1 * t2 * ...)`
- Option: `'a option`
- Map (string-keyed alias): `'a map`
Expand Down Expand Up @@ -68,6 +69,16 @@ This document specifies the value and type system used by the interpreter.
- `Some : 'a -> 'a option`
- `None : 'a option`

### Tasks
- Type form: `'a task`
- Construction and observation:
- `Task.spawn : (unit -> 'a) -> 'a task`
- `Task.await : 'a task -> 'a`
- Notes:
- tasks are opaque runtime handles
- spawned thunks execute concurrently
- side effects may interleave across tasks

## Function types
- Functions use curried arrow types:
- `t1 -> t2`
Expand Down Expand Up @@ -117,6 +128,7 @@ This document specifies the value and type system used by the interpreter.
- `VRecord`
- `VMap`
- `VOption`
- `VTask`
- `VUnionCase`, `VUnionCtor`
- `VClosure`
- `VExternal`
Expand Down
40 changes: 40 additions & 0 deletions samples/parallel-quicksort.fss
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
let rec quicksort values =
match values with
| [] -> []
| pivot :: rest ->
let smaller = rest |> List.filter (fun value -> value < pivot)
let equal = rest |> List.filter (fun value -> value = pivot)
let greater = rest |> List.filter (fun value -> value > pivot)
(quicksort smaller) @ [pivot] @ equal @ (quicksort greater)

let rec parallel_quicksort values =
match values with
| [] -> []
| [single] -> [single]
| pivot :: rest ->
let smaller = rest |> List.filter (fun value -> value < pivot)
let equal = rest |> List.filter (fun value -> value = pivot)
let greater = rest |> List.filter (fun value -> value > pivot)

let sort_smaller =
Task.spawn (fun _ -> parallel_quicksort smaller)

let sorted_greater = parallel_quicksort greater
let sorted_smaller = Task.await sort_smaller
sorted_smaller @ [pivot] @ equal @ sorted_greater

let rec is_sorted values =
match values with
| [] -> true
| [_] -> true
| first :: second :: tail -> (first <= second) && (is_sorted (second :: tail))

let input = [9; 2; 8; 2; 4; 7; 1; 6; 3; 5; 3; 10; 0; 12; 11]
let sequential = quicksort input
let parallel = parallel_quicksort input

print $"parallel quicksort input : {input}"
print $"sequential quicksort output : {sequential}"
print $"parallel quicksort output : {parallel}"
print $"same result : {sequential = parallel}"
print $"parallel output is sorted : {is_sorted parallel}"
7 changes: 7 additions & 0 deletions src/FScript.CSharpInterop/LanguageServer/LspHandlers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ module LspHandlers =
"fun"; "raise"; "import"; "export"; "qualified" ]
|> Set.ofList

let private builtinTypeTokenSet =
[ "unit"; "int"; "float"; "bool"; "string"
"list"; "option"; "map"; "task"
"Environment"; "FsKind" ]
|> Set.ofList

let private classifyToken (line: string) (startIndex: int) (token: string) =
let isFunctionCallToken () =
let mutable i = startIndex + token.Length
Expand All @@ -120,6 +126,7 @@ module LspHandlers =
i < line.Length && line[i] = '('

if keywordSet.Contains(token) then 0
elif builtinTypeTokenSet.Contains(token) then 4
elif token.Length > 1 && token.StartsWith("\"") && token.EndsWith("\"") then 1
elif token |> Seq.forall Char.IsDigit then 2
elif isFunctionCallToken () then 3
Expand Down
3 changes: 3 additions & 0 deletions src/FScript.CSharpInterop/LanguageServer/LspSymbols.fs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module LspSymbols =
| TBool -> "bool"
| TString -> "string"
| TList t1 -> sprintf "%s list" (postfixArg t1)
| TTask t1 -> sprintf "%s task" (postfixArg t1)
| TTuple ts -> ts |> List.map go |> String.concat " * " |> sprintf "(%s)"
| TRecord fields ->
fields
Expand Down Expand Up @@ -67,6 +68,8 @@ module LspSymbols =
"String.concat", [ "separator"; "values" ]
"String.split", [ "separator"; "source" ]
"String.endsWith", [ "suffix"; "source" ]
"Task.spawn", [ "thunk" ]
"Task.await", [ "task" ]
"List.empty", []
"List.map", [ "mapper"; "values" ]
"List.iter", [ "iterator"; "values" ]
Expand Down
28 changes: 28 additions & 0 deletions src/FScript.Language/BuiltinFunctions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ module BuiltinFunctions =
| [ VList value ] -> value
| _ -> fail $"{functionName} expects (list)"

let private expectTask functionName args =
match args with
| [ VTask value ] -> value
| _ -> fail $"{functionName} expects (task)"

let private asStringKey functionName value =
match value with
| VString key -> MKString key
Expand Down Expand Up @@ -255,6 +260,27 @@ module BuiltinFunctions =
| [ VString suffix; VString source ] -> VBool (source.EndsWith(suffix, StringComparison.Ordinal))
| _ -> fail "String.endsWith expects (string, string)") }

let private builtinTaskSpawn : ExternalFunction =
{ Name = "Task.spawn"
Scheme = scheme "Task.spawn"
Arity = 1
Impl =
(fun ctx args ->
match args with
| [ thunk ] -> ctx.SpawnTask thunk
| _ -> fail "Task.spawn expects (thunk)") }

let private builtinTaskAwait : ExternalFunction =
{ Name = "Task.await"
Scheme = scheme "Task.await"
Arity = 1
Impl =
(fun ctx args ->
let _ = expectTask "Task.await" args
match args with
| [ task ] -> ctx.AwaitTask task
| _ -> fail "Task.await expects (task)") }

let private builtinListEmpty : ExternalFunction =
{ Name = "List.empty"
Scheme = scheme "List.empty"
Expand Down Expand Up @@ -689,6 +715,8 @@ module BuiltinFunctions =
builtinStringConcat
builtinStringSplit
builtinStringEndsWith
builtinTaskSpawn
builtinTaskAwait
builtinListEmpty
builtinListMap
builtinListIter
Expand Down
3 changes: 3 additions & 0 deletions src/FScript.Language/BuiltinSignatures.fs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ module BuiltinSignatures =
"String.concat", Forall([], TFun(TString, TFun(TList TString, TString)))
"String.split", Forall([], TFun(TString, TFun(TString, TList TString)))
"String.endsWith", Forall([], TFun(TString, TFun(TString, TBool)))
"Task.spawn", Forall([ 0 ], TFun(TFun(TUnit, TVar 0), TTask(TVar 0)))
"Task.await", Forall([ 0 ], TFun(TTask(TVar 0), TVar 0))
"List.empty", Forall([ 0 ], TList(TVar 0))
"List.map", Forall([ 0; 1 ], TFun(TFun(TVar 0, TVar 1), TFun(TList(TVar 0), TList(TVar 1))))
"List.iter", Forall([ 0 ], TFun(TFun(TVar 0, TUnit), TFun(TList(TVar 0), TUnit)))
Expand Down Expand Up @@ -60,6 +62,7 @@ module BuiltinSignatures =
|> Map.keys
|> Seq.filter (fun name ->
name.StartsWith("List.", System.StringComparison.Ordinal)
|| name.StartsWith("Task.", System.StringComparison.Ordinal)
|| name.StartsWith("Option.", System.StringComparison.Ordinal)
|| name.StartsWith("Map.", System.StringComparison.Ordinal))
|> Set.ofSeq
Loading
Loading