Interactive Development

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.

AspectF# Interactive (FSI)Clef Interactive (fsni)
ExecutionCLR JITNative interpretation or AOT
MemoryGC-managedDeterministic (arena-based)
Script extension.fsx.fsnx
Entry commanddotnet fsifsni
Directive prefix#r, #load#require, #load

Design Principles

  1. Familiar Experience: Developers moving from managed F# should find fsni familiar
  2. Native Semantics: Interactive execution follows native memory and type semantics
  3. Tooling Integration: fsni integrates with the same LSP infrastructure as the compiler
  4. Exploratory Development: Support rapid prototyping and experimentation
  5. 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-x64

Session 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 = 102334155

Hybrid 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 -> int

Script Files

File Extension

Clef script files use the .fsnx extension:

script.fsnx      -- Clef script
module.fs        -- Clef implementation file
signature.fsi    -- Clef signature file

Rationale: Using a distinct extension (.fsnx rather 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

DirectiveDescription
#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 | hybridSet execution mode
#arena sizeSet arena size (e.g., #arena 128MB)
#target platformSet target platform
#helpDisplay help
#quitExit 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.fsnx

Memory 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 64MB

Arena 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 mode

Cross-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 value

This 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 $ x

When you enter an expression in fsni:

> [1; 2; 3];;

The system:

  1. Type-checks the expression (determines int list)
  2. Generates a display function via SRTP resolution
  3. Compiles both expression and display function
  4. 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 structure

For 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 time

Clef Note: Format specifiers %A and %O are 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:

ComponentManaged F#Clef
Compiler ServicesFCS (F# Compiler Services)CCS (Clef Compiler Service)
Language ServerFSAC (F# AutoComplete)FSNAC (Clef AutoComplete)
Package ManagerNuGetFargo (fpm)
Project Format.fsproj (MSBuild).fidproj (TOML)
Package Format.nupkg (binary).fidpkg (source)
InteractiveFSIfsni
Script Files.fsx.fsnx

Why Parallel Rather Than Plugin

The toolchains are parallel rather than plugins because:

  1. Type resolution fundamentally differs: CCS resolves string to native UTF-8 fat pointer semantics; FCS resolves to System.String. These cannot be reconciled at runtime.

  2. SRTP resolution differs: CCS resolves SRTP against native type witnesses; FCS resolves against BCL method tables.

  3. No obj escape hatch: Managed tooling uses obj as a universal container for values during type checking and display. Clef has no such type.

  4. 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.fs

IDE 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:

FeatureFableClefSharable?
Pure functions
Records, DUs
string operationsSystem.StringNativeStr
optionReference typevoption
printf "%A"ReflectionSRTP⚠️
AsyncAsync<'T>Native async
ReflectionAvailableNot 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: CryptoAlgorithms

This 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: MyLocalPackage

Tooling 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 list

Editor Integration

Editors supporting Clef (via Ionide or similar) provide:

  • Syntax highlighting for .fsnx files
  • 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.dev

Features:

  • 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:

FeatureAvailability
Core languageFull
Native libraryFull
File I/OSandboxed
NetworkRestricted
Custom native codeNot available
Execution timeLimited (30 seconds)
MemoryLimited (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 = 14

Diagnostics

CodeSeverityMessage
FS8500ErrorCannot execute cross-compiled code on host platform
FS8501WarningValue invalidated by arena reset
FS8502ErrorArena size exceeded; use #arena reset or increase size
FS8503WarningInterpretation mode may not reflect exact native behavior
FS8504InfoExpression compiled to native code
FS8505ErrorFreestanding 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

Featurefsniutop (OCaml)evcxr (Rust)Swift REPL
OfficialYesCommunityCommunityYes
ExecutionHybridBytecodeCompileJIT
Memory modelArenaGCOwnershipARC
Script files.fsnx.mlN/A.swift
NotebooksYesYesYesYes (Playgrounds)
Cross-compileYesLimitedNoNo

Areas Requiring Further Specification

  1. Interpretation semantics: Exact behavior of interpreter vs compiled code
  2. Arena lifecycle: Interaction between multiple scripts and arena management
  3. Debugging: Breakpoints and stepping in interactive mode
  4. Profiling: Performance analysis tools in fsni
  5. Package management: Integration with a native package manager
  6. Caching: Compilation caching for faster repeated execution
  7. State serialization: Saving and restoring session state

See Also