Interactive Development
This chapter specifies the interactive development experience for Clef, including the Clef Interactive environment (fsni), script execution, and integration with development tooling.
Overview
Clef provides an interactive development experience comparable to F# Interactive (FSI) in managed F#. The goal is to give Clef developers the same exploratory, REPL-driven workflow that .NET developers expect, while respecting the constraints of native compilation.
| Aspect | F# Interactive (FSI) | Clef Interactive (fsni) |
|---|---|---|
| Execution | CLR JIT | Native interpretation or AOT |
| Memory | GC-managed | Deterministic (arena-based) |
| Script extension | .fsx | .fsnx |
| Entry command | dotnet fsi | fsni |
| Directive prefix | #r, #load | #require, #load |
Design Principles
- Familiar Experience: Developers moving from managed F# should find fsni familiar
- Native Semantics: Interactive execution follows native memory and type semantics
- Tooling Integration: fsni integrates with the same LSP infrastructure as the compiler
- Exploratory Development: Support rapid prototyping and experimentation
- Seamless Transition: Code developed interactively should compile without modification
Clef Interactive (fsni)
Invocation
The Clef Interactive environment is invoked via the fsni command:
# Start interactive session
fsni
# Execute a script file
fsni script.fsnx
# Evaluate an expression
fsni --eval "1 + 1"
# With specific platform target
fsni --target linux-x64Session Model
An fsni session maintains:
- A global environment of bound values and types
- An arena for interactive allocations
- A history of evaluated expressions
- Loaded modules and dependencies
Clef Interactive (fsni) v1.0
Target: linux-x64
Arena: 64MB (expandable)
> let x = 42;;
val x : int = 42
> let greet name = $"Hello, {name}!";;
val greet : string -> string
> greet "World";;
val it : string = "Hello, World!"Execution Model
fsni supports multiple execution strategies:
Interpretation Mode (Default)
Expressions are interpreted without full native compilation. This provides:
- Fast feedback for simple expressions
- Lower latency than full compilation
- Suitable for exploration and prototyping
> #mode interpret;;
Execution mode: interpret
> [1..1000] |> List.map (fun x -> x * x);;
val it : int list = [1; 4; 9; 16; ...]Compilation Mode
Expressions are compiled to native code and executed. This provides:
- Accurate performance characteristics
- Full optimization
- Behavior identical to compiled programs
> #mode compile;;
Execution mode: compile (target: linux-x64)
> let rec fib n = if n < 2 then n else fib (n-1) + fib (n-2);;
val fib : int -> int
-- Compiled to native code
> #time on;;
> fib 40;;
Real: 00:00:00.892
val it : int = 102334155Hybrid Mode
The default for production use. Simple expressions are interpreted; complex definitions are compiled.
> #mode hybrid;;
Execution mode: hybrid
> let x = 1 + 1;; // Interpreted
val x : int = 2
> let rec factorial n = // Compiled (recursive)
if n <= 1 then 1
else n * factorial (n - 1);;
val factorial : int -> intScript Files
File Extension
Clef script files use the .fsnx extension:
script.fsnx -- Clef script
module.fs -- Clef implementation file
signature.fsi -- Clef signature fileRationale: Using a distinct extension (
.fsnxrather than.fsx) clearly identifies scripts intended for native execution and avoids confusion with managed F# scripts.
Script Structure
A script file contains a sequence of declarations and expressions:
// script.fsnx
#load "helpers.fs"
open Console
let data = [1; 2; 3; 4; 5]
let sum = List.fold (+) 0 data
writeln $"Sum: {sum}"Script Directives
| Directive | Description |
|---|---|
#require "name" | Load a package dependency |
#load "file.fs" | Load and compile an F# source file |
#load "file.fsnx" | Load and execute another script |
#time "on" | "off" | Toggle timing display |
#mode interpret | compile | hybrid | Set execution mode |
#arena size | Set arena size (e.g., #arena 128MB) |
#target platform | Set target platform |
#help | Display help |
#quit | Exit the session |
Shebang Support
Script files may include a shebang for direct execution:
#!/usr/bin/env fsni
// script.fsnx
open Console
writeln "Hello from Clef!"chmod +x script.fsnx
./script.fsnxMemory Model in Interactive Mode
Arena-Based Allocation
Interactive sessions use arena-based memory management:
> #arena 64MB;;
Arena size: 64MB
> let bigList = [1..1000000];;
val bigList : int list
-- Allocated in session arena
> #arena status;;
Arena: 12.4MB used of 64MBArena Reset
The arena can be reset to reclaim memory:
> #arena reset;;
Arena reset. All interactive values invalidated.
> bigList;;
Error: Value 'bigList' is no longer valid after arena reset.Persistent Values
Values can be marked as persistent to survive arena resets:
> #persist let config = loadConfig();;
val config : Config [persistent]
> #arena reset;;
Arena reset. Persistent values retained.
> config;; // Still valid
val it : Config = { ... }Platform Targeting
Target Selection
fsni can target different platforms:
> #target linux-x64;;
Target: linux-x64
> #target linux-arm64;;
Target: linux-arm64
> #target freestanding-arm-none-eabi;;
Target: freestanding-arm-none-eabi
-- Note: Limited library support in freestanding modeCross-Compilation in Interactive Mode
When targeting a different platform than the host:
> #target linux-arm64;;
Target: linux-arm64 (cross-compiling from linux-x64)
Execution mode: compile-only (cannot execute on host)
> let add x y = x + y;;
val add : int -> int -> int
-- Compiled for linux-arm64, not executed
> add 1 2;;
Warning: Cannot execute arm64 code on x64 host.
Use #target linux-x64 to execute, or #emit to generate binary.
> #emit "add.o";;
Emitted: add.o (linux-arm64)Value Display Without Runtime Reflection
A fundamental difference between fsni and managed F# Interactive (FSI) is how values are formatted for display.
The Managed F# Approach
In FSI, value display relies on obj and runtime reflection:
// Managed FSI internals (simplified)
let displayValue (value: obj) : string =
sprintf "%A" value // Uses reflection to inspect valueThis approach is not available in Clef because:
- There is no universal base type
obj - There is no runtime type information or reflection
- Values cannot be “boxed” to a common representation
SRTP-Based Value Formatting
Clef uses statically resolved type parameters (SRTP) to generate formatters at compile time:
// fsni generates specific formatters via SRTP
type Displayable = Displayable
with static member inline ($) (Displayable, x: int) =
Text.Format.intToString x
static member inline ($) (Displayable, x: string) =
"\"" + x + "\""
static member inline ($) (Displayable, xs: 'T list) =
"[" + (xs |> List.map (fun x -> Displayable $ x)
|> String.concat "; ") + "]"
// ... additional overloads for all displayable types
let inline display x = Displayable $ xWhen you enter an expression in fsni:
> [1; 2; 3];;The system:
- Type-checks the expression (determines
int list) - Generates a display function via SRTP resolution
- Compiles both expression and display function
- Executes and formats the result
Implications for Custom Types
User-defined types require explicit display support:
> type Point = { X: float; Y: float };;
type Point = { X: float; Y: float }
> { X = 1.0; Y = 2.0 };;
val it : Point = { X = 1.0; Y = 2.0 }
-- Display generated from record field structureFor types requiring custom formatting:
> type Point = { X: float; Y: float }
with static member ($) (Displayable, p: Point) =
$"({p.X}, {p.Y})";;
> { X = 1.0; Y = 2.0 };;
val it : Point = (1.0, 2.0)Format Specifiers
The %A format specifier in Clef uses SRTP rather than reflection:
> printfn "%A" [1; 2; 3];;
[1; 2; 3]
-- SRTP resolves formatting at compile timeClef Note: Format specifiers
%Aand%Oare resolved at compile time via SRTP. Types must have appropriate formatting members resolvable statically. See Native Type Mappings.
Tooling Architecture
Parallel Toolchain Model
Clef uses a parallel toolchain rather than extending managed F# tooling:
| Component | Managed F# | Clef |
|---|---|---|
| Compiler Services | FCS (F# Compiler Services) | CCS (Clef Compiler Service) |
| Language Server | FSAC (F# AutoComplete) | FSNAC (Clef AutoComplete) |
| Package Manager | NuGet | Fargo (fpm) |
| Project Format | .fsproj (MSBuild) | .fidproj (TOML) |
| Package Format | .nupkg (binary) | .fidpkg (source) |
| Interactive | FSI | fsni |
| Script Files | .fsx | .fsnx |
Why Parallel Rather Than Plugin
The toolchains are parallel rather than plugins because:
Type resolution fundamentally differs: CCS resolves
stringto native UTF-8 fat pointer semantics; FCS resolves toSystem.String. These cannot be reconciled at runtime.SRTP resolution differs: CCS resolves SRTP against native type witnesses; FCS resolves against BCL method tables.
No
objescape hatch: Managed tooling usesobjas a universal container for values during type checking and display. Clef has no such type.Source-based packages: Fargo distributes source code for whole-program optimization. NuGet distributes compiled binaries.
Coexistence with Fable and Managed F#
Clef tooling is designed to coexist with other F# targets in a single workspace:
my-project/
├── web-ui/ # Fable → JavaScript
│ ├── App.fsproj # ← FSAC handles this
│ └── Components.fs
├── native-backend/ # Clef → Native binary
│ ├── Server.fidproj # ← FSNAC handles this
│ └── Api.fs
└── shared/ # Pure F# domain types
├── Shared.fsproj # ← Both can consume (with constraints)
└── Domain.fsIDE integration (Ionide) routes to the appropriate language server based on project type:
.fsproj→ FSAC (managed F# or Fable).fidproj→ FSNAC (Clef)
Shared Code Constraints
Code shared between Fable and Clef must avoid:
| Feature | Fable | Clef | Sharable? |
|---|---|---|---|
| Pure functions | ✅ | ✅ | ✅ |
| Records, DUs | ✅ | ✅ | ✅ |
string operations | System.String | NativeStr | ❌ |
option | Reference type | voption | ❌ |
printf "%A" | Reflection | SRTP | ⚠️ |
| Async | Async<'T> | Native async | ❌ |
| Reflection | Available | Not available | ❌ |
Shared code should be restricted to pure domain modeling without IO or string manipulation.
Package Management Integration
Fargo Integration
fsni integrates with Fargo for package management:
> #require "robot-controller";;\n-- Resolving robot-controller from frgo.dev...\n-- Downloaded: robot-controller-1.2.0.fidpkg\n-- Source files: 12\n-- Compiling for current session...\nLoaded: RobotController (3 modules)
> open RobotController.Algorithms;;
> PIDController.create 1.0 0.1 0.05;;
val it : PIDController = { Kp = 1.0; Ki = 0.1; Kd = 0.05 }Source-Based Loading
Unlike NuGet which loads pre-compiled binaries, Fargo loads source:
> #require "crypto-algorithms";;\n-- Source package: crypto-algorithms-2.0.0\n-- Compiling with current target optimizations...\n-- Inlining enabled across package boundary\nLoaded: CryptoAlgorithmsThis enables:
- Cross-package inlining
- Target-specific optimization
- Dead code elimination
- Consistent native semantics
Local Package Development
For local package development:
> #require "path: ../my-local-package";;
-- Loading from: /home/dev/my-local-package
-- Watching for changes...
Loaded: MyLocalPackage
> // Edit my-local-package source files...
> #reload;;
-- Reloading changed packages...
-- MyLocalPackage: 2 files changed
Reloaded: MyLocalPackageTooling Integration
LSP Integration
fsni connects to the Clef Language Server (FSNAC) for:
- Autocompletion in the REPL
- Type information on hover
- Error diagnostics
- Go-to-definition for loaded modules
> List.ma<TAB>
List.map : ('a -> 'b) -> 'a list -> 'b list
List.map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
List.mapi : (int -> 'a -> 'b) -> 'a list -> 'b listEditor Integration
Editors supporting Clef (via Ionide or similar) provide:
- Syntax highlighting for
.fsnxfiles - Inline evaluation (evaluate selection in fsni)
- Hover types
- Error underlining
- Send-to-REPL functionality
Notebook Support
fsni supports notebook interfaces (Jupyter, Polyglot Notebooks):
{
"kernelspec": {
"name": "clef",
"display_name": "Clef",
"language": "fsharp"
}
}Web Playground
Online Interactive Environment
A web-based playground provides interactive Clef execution:
try.fidelity.devFeatures:
- Browser-based editor with syntax highlighting
- Server-side compilation and execution
- Shareable code snippets
- Example gallery
- Platform target selection
Playground Limitations
The web playground operates with restrictions:
| Feature | Availability |
|---|---|
| Core language | Full |
| Native library | Full |
| File I/O | Sandboxed |
| Network | Restricted |
| Custom native code | Not available |
| Execution time | Limited (30 seconds) |
| Memory | Limited (256MB arena) |
Interoperability
Loading Compiled Modules
fsni can load pre-compiled Clef modules:
> #load-native "mylib.fno";;
Loaded: MyLib (5 modules, 42 functions)
> MyLib.Utilities.process data;;
val it : Result<Data, Error> = Ok { ... }FFI in Interactive Mode
Foreign function interfaces work in compile mode:
> #mode compile;;
> [<PlatformBinding>]
module Native =
let puts : string -> int = native "puts";;
> Native.puts "Hello from C!";;
Hello from C!
val it : int = 14Diagnostics
| Code | Severity | Message |
|---|---|---|
| FS8500 | Error | Cannot execute cross-compiled code on host platform |
| FS8501 | Warning | Value invalidated by arena reset |
| FS8502 | Error | Arena size exceeded; use #arena reset or increase size |
| FS8503 | Warning | Interpretation mode may not reflect exact native behavior |
| FS8504 | Info | Expression compiled to native code |
| FS8505 | Error | Freestanding target has limited library support |
Grammar
script-file :=
shebang-line? script-directive* module-elems
shebang-line :=
#! filepath newline
script-directive :=
# require string
# load string
# time on-off
# mode exec-mode
# arena arena-spec
# target platform-spec
# persist let-binding
# help
# quit
exec-mode :=
interpret
compile
hybrid
arena-spec :=
size-literal
reset
status
on-off :=
"on"
"off"Comparison with Other Native REPLs
| Feature | fsni | utop (OCaml) | evcxr (Rust) | Swift REPL |
|---|---|---|---|---|
| Official | Yes | Community | Community | Yes |
| Execution | Hybrid | Bytecode | Compile | JIT |
| Memory model | Arena | GC | Ownership | ARC |
| Script files | .fsnx | .ml | N/A | .swift |
| Notebooks | Yes | Yes | Yes | Yes (Playgrounds) |
| Cross-compile | Yes | Limited | No | No |
Areas Requiring Further Specification
- Interpretation semantics: Exact behavior of interpreter vs compiled code
- Arena lifecycle: Interaction between multiple scripts and arena management
- Debugging: Breakpoints and stepping in interactive mode
- Profiling: Performance analysis tools in fsni
- Package management: Integration with a native package manager
- Caching: Compilation caching for faster repeated execution
- State serialization: Saving and restoring session state
See Also
- Program Structure and Execution - Compiled program execution
- Memory Regions - Arena memory model
- Error Handling - Tooling integration model
- Platform Bindings - FFI in interactive mode