The promise of functional programming has always been apparent: write code that expresses a process to an end result, not how the machine should perform those actions. Yet for decades, this elegance came with a tax - runtime overhead, garbage collection pauses, and the implicit assumption that “real” systems programming belonged to C and its descendants. The Fidelity Framework challenges this assumption by asking a different question:
What if we could preserve F#’s expressiveness, safety and precision while compiling to native code that rivals hand-written C in efficiency?
Zero-Cost Functional Programming
The Firefly compiler represents the technical heart of the Fidelity Framework - a vision of how functional code becomes machine code. Unlike traditional F# compilation to .NET bytecode, Firefly orchestrates a sophisticated lowering process through multiple intermediate representations, each preserving critical type information while progressively approaching native platform instructions.
& Dependency Analysis] AST --> FCS[F# Compiler Services] MLIR[MLIR High-Level Dialects] PAST[Fully Processed AST] LLVM[LLVM Lowering and
Link-Time Optimization] LLVM --> NATIVE[Native Binary] FCS -.-> TYPE[Type-aware Pruning
& Reachability Analysis] TYPE -.-> PAST PAST --> MLIR MLIR --> OPT[Progressive Optimization] OPT --> LLVM AST -.->|C/C++
static binding info| LLVM style FSX fill:#f9f,stroke:#333,stroke-width:2px style NATIVE fill:#9f9,stroke:#333,stroke-width:2px
What makes this approach revolutionary isn’t just the elimination of runtime overhead - it’s the preservation of F#’s rich type information throughout the compilation pipeline. Types guide optimization decisions, enable safety verifications, and ensure memory layouts match exactly what native code expects, all while compiling away to nothing in the final binary. This is what is meant by “zero cost abstraction”. All of that structure is preserved throughout compilation until it’s no longer needed at the final stages of “lowering”.
A Tale of Four Programs
To understand some early dimensions of how Firefly transforms functional elegance into native efficiency, let’s follow four variations of a simple “Hello World” program. Each version introduces new challenges that reveal how the compiler handles increasingly sophisticated F# idioms.
Level 1: Direct and Imperative
Our journey begins with HelloWorldDirect.fs
:
open Alloy
open Alloy.Console
open Alloy.Text.UTF8
open Alloy.Memory
let hello() =
use buffer = stackBuffer<byte> 256
Prompt "Enter your name: "
let name =
match readInto buffer with
| Ok length -> spanToString (buffer.AsSpan(0, length))
| Error _ -> "Unknown Person"
let message = sprintf "Hello, %s!" name
WriteLine message
[<EntryPoint>]
let main argv =
hello()
0
Even this “simple” example demonstrates the depth of Firefly’s compilation challenge. The function begins with stack allocation (stackBuffer<byte> 256
), performs console I/O (prompt
and readInto
), handles error cases with pattern matching, and converts between different string representations (spanToString
). Each of these operations must compile to efficient native code without heap allocations.
But let’s focus on the final two lines - the seemingly straightforward formatting and output:
let message = sprintf "Hello, %s!" name
writeLine message
This straightforward code might seem trivial, but it exercises fundamental compiler capabilities. The F# AST here contains simple function applications and variable bindings. When Firefly encounters this, it generates MLIR that directly maps each operation:
%message = func.call @sprintf(%format_str, %name) : (!llvm.ptr, !llvm.ptr) -> !llvm.ptr
func.call @writeLine(%message) : (!llvm.ptr) -> ()
The compiler’s job here is relatively mechanical - each F# expression becomes a corresponding MLIR operation. Variable bindings become SSA values, function calls become func.call
operations. This direct mapping validates that our basic compilation pipeline works correctly.
Level 2: Inline Expressions
The second version, HelloWorldSaturated.fs
, removes the intermediate variable:
writeLine (sprintf "Hello, %s!" name)
Now the compiler faces nested expressions. The F# AST represents this as:
App(writeLine, App(sprintf, "Hello, %s!", name))
Firefly must generate temporary SSA values for the intermediate results:
%0 = func.call @sprintf(%format_str, %name) : (!llvm.ptr, !llvm.ptr) -> !llvm.ptr
func.call @writeLine(%0) : (!llvm.ptr) -> ()
The compiler demonstrates its ability to manage evaluation order and temporary values without explicit variable names. This seemingly small step validates crucial infrastructure for handling complex expressions.
Level 3: The Pipeline Challenge
HelloWorldHalfCurried.fs
introduces F#’s beloved pipeline operator:
name
|> format "Hello, %s!"
|> writeLine
This is where things get interesting. The pipeline operator is syntactic sugar for function application, but format
is a curried function. When partially applied with the template string, it returns a function waiting for its second argument.
For Firefly to handle this, it needs to:
- Recognize curried function signatures
- Generate code for partial application
- Transform the partial application into direct calls
- Maintain type safety throughout
The MLIR generation would need to transform the curried calls into something like:
// Conceptual transformation - not yet implemented
%template = llvm.mlir.addressof @.str.hello : !llvm.ptr
%formatted = func.call @format(%template, %name) : (!llvm.ptr, !llvm.ptr) -> !llvm.ptr
func.call @writeLine(%formatted) : (!llvm.ptr) -> ()
Level 4: Full Functional Power
The final version, HelloWorldFullCurried.fs
, showcases F#’s full functional programming capabilities:
open Alloy
open Alloy.Console
open Alloy.Text
open Alloy.Text.UTF8
open Alloy.Memory
let hello greetingFormat =
use buffer = stackBuffer<byte> 256
Prompt "Enter your name: "
readInto buffer
|> Result.map (fun length ->
buffer.AsSpan(0, length)
|> spanToString)
|> Result.defaultValue "Unknown Person"
|> fun name -> sprintf greetingFormat name
|> WriteLine
[<EntryPoint>]
let main argv =
match argv with
| [|greetingFormat|] -> hello greetingFormat
| _ -> hello "Hello, %s!"
0
This code demonstrates:
- Lambda expressions with closure capture (the lambda closes over
buffer
) - Higher-order functions (
Result.map
) - Monadic operations (Result type)
- Multiple levels of pipelining
The compilation challenge here is substantial. The compiler must:
- Transform the lambda into a concrete function
- Handle closure capture without heap allocation
- Inline or optimize away the Result operations
- Maintain type information for safety verification
Static Resolution in Alloy
What many .NET developers don’t realize is that F# already contains all the tools necessary to describe a “bare metal world” through features like FSharp.NativeInterop
. The Alloy library leverages these capabilities to create a BCL-like foundation that compiles to zero-allocation native code. This is our “innovation budget” at work - preserving familiar F# idioms at the application level while embracing the broader spectrum of F# syntax for static resolution underneath.
Consider how dependencies flow through our most complex example:
AutoOpen] --> MEM MATH --> MEM MATH --> TEXT subgraph "Math Module" MATH --> MATHFN[Math.Functions] MATH --> MATHOP[Math.Operators] MATH --> MATHNM[Math.NativeMath] end subgraph "Text Module" TEXT --> UTF8[Text.UTF8] end UTF8 --> CONS CORE --> HW[HelloWorldFullCurried] CONS --> HW TEXT --> HW UTF8 --> HW MEM --> HW style NI fill:#f9f9f9,stroke:#333,stroke-width:2px style CORE fill:#ffe6e6,stroke:#333,stroke-width:3px style HW fill:#e6f3ff,stroke:#333,stroke-width:3px style MATH fill:#fff0e6,stroke:#333,stroke-width:2px style TEXT fill:#e6ffe6,stroke:#333,stroke-width:2px style MEM fill:#f0e6ff,stroke:#333,stroke-width:2px style CONS fill:#ffffe6,stroke:#333,stroke-width:2px
This dependency graph reveals the “iceberg model” in action (with the flow diagram inverted). At the “surface” - our HelloWorld application - we see familiar F# patterns: pipelines, Result types, and string formatting. But behind that surface lies a sophisticated static resolution system that would surprise many F# developers.
The Art of Static Resolution
The Alloy library demonstrates patterns that, while perfectly valid F#, aren’t commonly seen in typical .NET applications:
// In Alloy.Core - Statically resolved operators
let inline (=) (x: ^T) (y: ^T) : bool
when ^T : equality =
FSharp.Core.Operators.(=) x y
// In Alloy.Memory - Zero-allocation buffer management
type INativeBuffer<'T when 'T : unmanaged> =
abstract member Pointer : nativeptr<'T>
abstract member Length : int
abstract member Item : int -> 'T with get, set
// In Alloy.Numerics - Compile-time arithmetic resolution
let inline add (x: ^T) (y: ^T) : ^T
when ^T : (static member (+) : ^T * ^T -> ^T) =
x + y
These patterns leverage F#’s statically resolved type parameters (SRTPs) to ensure all operations resolve at compile time. No virtual dispatch, no interface lookups, no runtime type checks. Every operation compiles down to direct machine instructions.
A BCL “Shadow” Shedding Light On Native
The Alloy library will eventually serve as a “shadow API” to familiar BCL operations, but with a crucial difference - everything must be statically resolvable:
// BCL style (allocates)
let message = String.Format("Hello, {0}!", name)
// Alloy style (zero allocation)
let message = name |> format "Hello, %s!"
// Compiles to direct sprintf call with stack buffers
Alloy will eventually emerge as a full replacement for conventional .NET Base Class Library capabilities, with an emphasis on zero-allocation systems programming. Unlike the BCL’s object-oriented hierarchy with its deep namespace nesting and heap-based abstractions, Alloy provides a flat, functional API where modules map directly to system capabilities. This design philosophy extends throughout the framework, making common operations more discoverable while ensuring every function compiles to predictable, allocation-free native code:
Alloy.Core
provides fundamental operations with zero-allocation option types and basic collection operationsAlloy.Memory
offers stack-based memory management throughSpan<'T>
andStackBuffer<'T>
types without heap allocationAlloy.Console
implements console I/O operations that compile to direct system calls, with integrated printf-style formatting and support for multiple platformsAlloy.Math
provides mathematical operations through bothFunctions
andOperators
submodules for flexibility in namespace and library inclusion managementAlloy.Text
provides zero-copy UTF-8 encoding and string-to-bytes conversions
Each module carefully uses F# features that might seem esoteric to application developers but are essential for systems programming: inline functions, SRTPs, native pointers, and stack-allocated buffers with compile-time bounds checking.
Over time the community will help shape and expand these library modules into a proper balance of “shadow API” versus new convenience patterns for native compilation. The goal here isn’t to make a replacement for .NET (or Fable for that matter). The objective of providing Alloy is to balance familiarity with new levels of accessibility to target a much wider variety of systems and compute architectures.
To be clear, “leaning into” F#’s origins makes sense in this early stage of the framework. When boundary cases become evident then the Firefly compiler and Fidelity framework writ large will take on its own characterization of F# that fits this emergent operational model. The idea is to do as much as possible to manage the “innovation budget” around this framework to ease adoption in what may at first be considered a radical departure from runtime-managed operation.
The Guiding Hand: From AST to Silicon
What makes Firefly unique isn’t just that it compiles F# to native code - it’s how it guides the transformation through multiple representations, each serving a specific purpose:
F# AST: Preserving Intent
The F# Compiler Services provides a fully type-checked AST that captures the programmer’s intent with complete type information. Every expression knows its type, every function knows its signature. This rich semantic information becomes our North Star throughout compilation.
MLIR: The Transformation Engine
MLIR (Multi-Level Intermediate Representation) allows Firefly to express high-level concepts that don’t exist in traditional compiler IRs. We can represent F# specific patterns like:
- Discriminated unions as MLIR types
- Pattern matching as structured control flow
- Curried functions as first-class concepts
- Memory safety invariants as attributes
The progressive lowering through MLIR dialects allows us to verify properties at each level:
// High-level: F# concepts preserved using standard MLIR dialects
func.func @process_result(%input: !llvm.struct<(i1, i64, !llvm.ptr)>) -> !llvm.ptr {
// Extract discriminator for Result type
%is_ok = llvm.extractvalue %input[0] : !llvm.struct<(i1, i64, !llvm.ptr)>
// Pattern match using control flow dialect
cf.cond_br %is_ok, ^ok_case, ^error_case
^ok_case:
%value = llvm.extractvalue %input[1] : !llvm.struct<(i1, i64, !llvm.ptr)>
%template = llvm.mlir.addressof @.str.greeting : !llvm.ptr
// Curried function represented as nested calls
%buffer = memref.alloca() : memref<256xi8>
%result = func.call @format(%template, %value, %buffer) : (!llvm.ptr, i64, memref<256xi8>) -> !llvm.ptr
cf.br ^exit(%result : !llvm.ptr)
^error_case:
%err_ptr = llvm.extractvalue %input[2] : !llvm.struct<(i1, i64, !llvm.ptr)>
cf.br ^exit(%err_ptr : !llvm.ptr)
^exit(%final: !llvm.ptr):
return %final : !llvm.ptr
}
// After lowering: Closer to machine representation
llvm.func @process_result(%input: !llvm.ptr) -> !llvm.ptr {
// Load discriminator from Result struct
%is_ok_ptr = llvm.getelementptr %input[0, 0] : (!llvm.ptr) -> !llvm.ptr
%is_ok = llvm.load %is_ok_ptr : !llvm.ptr -> i1
llvm.cond_br %is_ok, ^ok, ^error
^ok:
%value_ptr = llvm.getelementptr %input[0, 1] : (!llvm.ptr) -> !llvm.ptr
%value = llvm.load %value_ptr : !llvm.ptr -> i64
%template = llvm.mlir.addressof @.str.greeting : !llvm.ptr
// Direct sprintf call after currying is resolved
%buffer = llvm.alloca %c256 x i8 : (i64) -> !llvm.ptr
%formatted = llvm.call @sprintf(%buffer, %template, %value) : (!llvm.ptr, !llvm.ptr, i64) -> i32
llvm.br ^exit(%buffer : !llvm.ptr)
^error:
%err_ptr = llvm.getelementptr %input[0, 2] : (!llvm.ptr) -> !llvm.ptr
%err = llvm.load %err_ptr : !llvm.ptr -> !llvm.ptr
llvm.br ^exit(%err : !llvm.ptr)
^exit(%result: !llvm.ptr):
llvm.return %result : !llvm.ptr
}
LLVM: A World Unto Itself
By the time code reaches LLVM IR, Firefly has transformed high-level F# concepts into low-level operations that LLVM’s many years of optimization work can aggressively improve. But crucially, type information has guided every transformation, ensuring safety properties are preserved even as abstractions are eliminated.
The Payoff: Zero-Cost Abstractions
The goal of this elaborate compilation pipeline isn’t academic - it’s to achieve something previously thought impossible: functional programming abstractions that produce efficient execution paths. When Firefly successfully compiles the full curried HelloWorld, the resulting assembly code will be substantially similar to what a C compiler would produce for equivalent imperative code, full type and memory safety “for free” in an execution context. Of course as shown here F# provides many approaches to achieve a given goal, and the Fidelity framework seeks to preserve as many of those options as possible within the wide range of idioms available within the F# lexicon.
The end result? No runtime types to track. No “boxing” overhead. No heap allocations to wrangle. Just clean, memory-safe machine code that respects the metal while preserving the elegance of functional programming for the developer.
Looking Forward: Beyond Hello World
These four HelloWorld examples represent more than variations on a theme - they’re milestones on the path to a new kind of systems programming. Each level of complexity we unlock in the compiler opens new possibilities:
- Level 1-2 Success: Basic applications, command-line tools
- Level 3 Success: F# idioms, making language constructs practical for systems programming
- Level 4 Success: Complex functional patterns, opening the door to domains like parsers and protocol implementations
The combination of Firefly’s compilation pipeline and Alloy’s static resolution patterns creates a unique capability: writing systems code that looks like idiomatic F# at the surface but compiles to bare metal efficiency underneath, all while preserving type and memory safety. This is the innovation budget well spent - complexity where it matters (in the compiler and base libraries) to preserve simplicity where developers work daily.
As we continue developing Firefly, each challenge that we overcome brings us closer to a future where choosing a technology stack isn’t about trading expressiveness for performance. In our case, with the Fidelity framework it’s about having both, with very few compromises. The journey from F# to native code isn’t just about compilation - it’s about re-imagining what’s possible when functional programming intelligently approaches bare metal.