This document describes the host external-function model, built-in extern catalog, and extension points.
The native interpreter external function is represented by:
Name: stringScheme: Scheme(polymorphic type scheme)Arity: intImpl: ExternalCallContext -> Value list -> Value
Registry entrypoint:
FScript.Runtime.Registry.all : HostContext -> ExternalFunction listHostContextincludes:RootDirectory : stringDeniedPathGlobs : string list(root-relative glob patterns hidden/blocked inside root)
- Optional plugin protocol marker:
[<FScript.Extensibility.FScriptExternProvider>]onpublic staticprovider methods in user assemblies- supported method signatures (all contract types below belong to
FScript.Extensibility):unit -> ExternalFunction listHostContext -> ExternalFunction listunit -> ExtensionRegistrationHostContext -> ExtensionRegistration
Interpreter integration:
- Extern schemes are injected into type inference environment.
- Extern values are injected into runtime environment as curried
VExternal.
LSP integration:
- The language server injects runtime extern schemes (
Registry.all) during document inference. - Hover/completion/signature help expose callable signatures for injected runtime functions and prelude stdlib functions.
- Injected stdlib functions use named-argument signatures when parameter names are available.
- Go-to-definition for stdlib injected functions resolves to readonly virtual stdlib sources (
fscript-stdlib:///...) for namespaced stdlib and runtime surfaces such asConsole.*,Task.*,Map.*, andList.*.
FScript.Extensibility.HostFunctions.create registration name handler returns the same ExternalFunction record with a generated closed scheme, curried arity, and conversions shared with HostTypes. Concrete synchronous F# handlers do not need to repeat Scheme, Arity, or Value pattern matches. Registered host descriptors remain explicit. Manual records remain available for capabilities outside this adapter. See Host function adapters for signature validation and invocation semantics.
Higher-order externs are implemented in runtime extern modules and receive an ExternalCallContext.
ExternalCallContext.Apply is a function pointer provided by the evaluator so extern code can apply script closures and curried functions without re-implementing evaluation.
Fs.readText : string -> string optionFs.exists : string -> boolFs.kind : string -> FsKindFs.createDirectory : string -> boolFs.writeText : string -> string -> boolFs.combinePath : string -> string -> stringFs.parentDirectory : string -> string optionFs.extension : string -> string optionFs.fileNameWithoutExtension : string -> stringFs.glob : string -> string list optionFs.enumerateFiles : string -> string -> string list optionFs.enumerateFilessearches only the requested directory when its pattern contains no path separator; use a path pattern such as**/*.fsfor recursive matching.- Denied-path policy:
Fs.globandFs.enumerateFileshide denied entries.Fs.globandFs.enumerateFilesdo not descend into denied or symlink/reparse-point directories.Fs.readText,Fs.writeText, andFs.createDirectorythrow an eval error on denied targets.Fs.existsreturnsfalseandFs.kindreturnsFsKind.Missingfor denied targets.
- Root-boundary policy:
- Paths are normalized before root checks.
- Symlink/reparse-point paths are treated as unsafe out-of-root paths.
Regex.matchGroups : string -> string -> string list option
Hash.md5 : string -> string optionGuid.new : 'a -> string option(dummy argument to preserve call shape)
Console.writeLine : string -> unitConsole.readLine : unit -> string option
-
Task.spawn : (unit -> 'a) -> 'a task -
Task.await : 'a task -> 'a -
These are part of the default runtime extern registry, so hosts may omit them.
List.*,Option.*, andMap.*helpers are provided by the embedded prelude inFScript.Language.- Scalar conversion helpers are built into the language runtime:
Int.tryParse : string -> int optionFloat.tryParse : string -> float optionBool.tryParse : string -> bool optionInt.toString : int -> stringFloat.toString : float -> stringBool.toString : bool -> stringString.replace : string -> string -> string -> string- argument order:
oldValue -> newValue -> source
- argument order:
String.indexOf : string -> string -> int option- argument order:
value -> source
- argument order:
String.toLower : string -> stringString.toUpper : string -> stringString.substring : int -> int -> string -> string option- argument order:
start -> length -> source
- argument order:
String.concat : string -> string list -> stringString.split : string -> string -> string list- argument order:
separator -> source
- argument order:
String.endsWith : string -> string -> bool
- Runtime externs focus on host/system capabilities.
FsKindis provided by stdlib as:FsKind.File of stringFsKind.Directory of stringFsKind.Missing
- See
stdlib-functions.mdfor the full stdlib function reference.
Json.deserialize : type -> string -> 'a optionJson.serialize : 'a -> string option- encoding supports scalars, unit/options, lists, tuples, records, visible host-managed fields, and maps with string/int/bool keys
- unsupported nested values (including unions, functions, tasks, and union-keyed maps) return
None
- JSON decoding uses the runtime token, with missing option fields mapped to
Noneand extra record properties ignored; unsupported targets, including opaque host-managed types, cannot be manufactured - The generic decoder scheme does not statically associate its token with the result; receiving functions can declare the intended return type
Xml.queryValues : string -> string -> string list option- argument order:
query -> xmldoc - query subset:
.../@attrand.../text()selectors only - supports simple element paths with optional leading
//descendant step - returns
Some []when query is valid but no values match - returns
Nonefor XML parse failure or invalid/unsupported query syntax
- argument order:
- Type/arity mismatch in extern calls raises eval errors.
- Options describe supported absence/conversion failures, not a universal exception boundary. JSON parsing and regex failures return
None; denied filesystem reads/writes raise errors, and reading an existing unreadable file can propagate I/O errors. Xml.queryValuesdoes not support XML namespaces.
Recommended steps:
- Add extern in a host module (
src/FScript.Runtime/*Externs.fs) withName/Scheme/Arity/Impl. - Register in
Registry.all. - For higher-order externs, use
ExternalCallContext.ApplyfromImplto invoke script functions. - Add tests in:
tests/FScript.Runtime.Testsfor direct module behavior.tests/FScript.Language.Tests/HostExternTests.fsfor interpreter integration.
- The
fscriptCLI can inject externs from user assemblies via:--extern-assembly <path>(repeatable)--no-default-externs(disablesRegistry.all)
- Provider discovery:
- The shared Runtime loader loads each unique assembly path once.
- It discovers methods annotated with
[<FScriptExternProvider>]. - It invokes each discovered provider once per environment initialization. CLI initializes before execution; VS Code initializes once per folder-server process.
- Conflicts:
- Any duplicate external function name across defaults + user providers is a fatal CLI error.
- Runtime constraints:
- If runtime assembly loading is unavailable (for example native AOT),
--extern-assemblyis rejected with a clear CLI error.
- If runtime assembly loading is unavailable (for example native AOT),
- External names are part of source-level API; renaming is breaking.
Providers may additionally return ExtensionRegistration (Types, Unions, and Functions), with the same zero-argument or HostContext invocation conventions. Host type metadata and opaque values use the existing extern execution bridge. SDK function-list providers are also supported. See host-managed types for creation restrictions, structural readers, payload transport, and boundary validation.
The CLI --config flag and VS Code environment selection reuse the runtime provider loader and its authoritative ExtensionRegistration. Both SDK function-list provider signatures are supported. See environment configuration for path resolution, merging, discovery, trust and reload behavior.
Assembly providers use FScript.Extensibility attributes and return SDK ExternalFunction list or ExtensionRegistration, with no arguments or one sealed SDK HostContext. Existing provider assemblies must rebuild. SDK helpers and values do not depend on interpreter assemblies. Runtime adapts registrations using ExtensionBridge.toLanguageRegistration; native embedding APIs retain their Language representations. Authoring helpers live in the SDK, not Runtime.
Provider trust, atomic loading, configuration selection, and reload rules are unchanged. SDK callback/task handles are valid only in their originating active evaluation; handler contexts expire on return. Runtime enforces limits and rejects foreign/expired handles. An SDK ExtensionException carries a message and optional inner cause; callback errors preserve script locations.
[[runtime.packages]] declares a required package name and optional exact version; an omitted version selects latest stable. The shared Runtime loader restores dependencies without the .NET SDK and discovers the existing SDK [FScriptExternProvider] methods in explicitly requested package runtime assemblies. Providers return the same SDK registrations used by local DLLs. EnvironmentLoader.loadWithInfo additionally exposes resolved package versions and concrete assembly paths; load retains its registration-only result. Parsing settings does not restore or execute packages. See environment configuration for cache, dependency selection, trust, and failure rules.
Purity is an opt-in, trusted host contract. Pure functions must be deterministic, effect-free, and terminating successfully for valid inputs, including any required runtime initialization. For higher-order bindings, this guarantee is conditional on pure, total callbacks; the optimizer checks callbacks conservatively. A returned callable does not inherit its factory's guarantee.
Pure external types promise deeply immutable visible data and opaque payloads, plus deterministic, effect-free, successfully terminating registered field reads, equality, and formatting. Constructors and ordinary methods require separate function contracts. Incorrect declarations are provider contract violations; compiler transformations may remove the claimed-pure computation.
Providers expose optional public static parameterless FScriptPureFunctions() and FScriptPureTypes() methods returning IEnumerable<string> of registered script names. The compiler CLI reads these for every compilation target. Invalid names, empty entries, and protected runtime contract overrides are rejected. Existing SDK records and ordinary runtime execution remain unchanged.
The .NET compiler API exposes PurityContracts, Compiler.compileWithPurity, Compiler.compileWithRegistrationAndPurity, and ClrBindings.providerPurity. Metadata survives resolved inference, HIR, LIR, serialization, and native linkage. No new FScript annotation syntax is introduced. See the provider guide for examples and optimization rules for limitations.
Optional FScriptThreadSafeTypes() and FScriptThreadSafeFunctions() provider
companions supply trusted concurrency contracts for opt-in task-sharing analysis.
They use BCL IEnumerable<string> registered names and do not change the SDK,
purity contracts, or runtime behavior. See
static task-sharing analysis.