The Full Frosty Experience

Platform-Aware Async Compilation Through Delimited Continuations
work-single-image

The Fidelity framework represents an ambitious project to bring F# to native compilation without runtime dependencies. One of the most challenging aspects of this endeavor is the treatment of asynchronous programming. This design document outlines our approach to compiling F#’s async computation expressions to efficient native code through delimited continuations, and introduces Frosty, an enhancement that brings advanced async patterns to this runtime-free environment.


🔄 Updated July 25, 2025

  • True RAII principles for automatic async resource management
  • Bidirectional PSG “zipper” computation expressions for async transformations
  • Integration with Olivier actor model for structured concurrency

…with special thanks to Paul Snively for his polyglot perspective that led to many of the connections drawn through the latest updates.


This document presents our current thinking and architectural plans. The designs described here are in various stages of development and prototyping, with implementation scheduled across multiple phases of the Fidelity project. We present these ideas to the F# community for feedback and discussion as we work toward making functional programming viable for systems-level development.

The Challenge: Async Without a Runtime

F#’s async computation expressions have long been one of the language’s most elegant features, predating similar constructs in other languages by many years. However, traditional async relies on sophisticated runtime infrastructure that presents fundamental challenges for systems programming:

// Traditional F# async - beautiful but runtime-dependent
let downloadAndProcess url = async {
    let! content = Http.downloadStringAsync url      // Heap-allocated Task
    let processed = processContent content           // Thread pool scheduling
    do! File.writeAllTextAsync "output.txt" processed // More allocations
    return processed.Length
}

// Problems for systems programming:
// - Tasks allocated on heap (GC pressure)
// - Thread pool scheduling (non-deterministic timing)
// - Exceptions for error handling (hidden control flow)
// - No compile-time resource guarantees

For Fidelity’s native compilation model, we need a fundamentally different approach - one that preserves F#’s elegant async programming model while enabling static analysis, deterministic resource management, and zero-allocation execution.

Understanding Resource Management Across Languages

Before diving into Frosty’s design, it’s crucial to understand how different languages approach resource management, as Fidelity aims to combine the best of all worlds:

C++ RAII: Automatic but Chaotic

// C++ RAII - automatic cleanup but no structure
void processFile() {
    std::ifstream file("data.txt");      // Constructor acquires
    std::vector<char> buffer(1024);      // Stack allocation
    
    processData(file, buffer);           
    // Both automatically cleaned up at scope exit
    // But: easy to create dangling references, no async story
}

.NET: Managed but Non-Deterministic

// F#/.NET - explicit disposal, GC-managed
let processFile() =
    use file = new FileStream("data.txt", FileMode.Open)  // IDisposable
    let buffer = Array.zeroCreate 1024  // Heap allocated, GC'd
    processData file buffer
    // file.Dispose() called, but buffer cleanup is non-deterministic
    
// Problems:
// - Must remember 'use' keyword
// - Finalizers are unpredictable
// - Can't use byrefs safely (GC can move memory)

Rust: Safe but Rigid

// Rust - borrow checker ensures safety but complex
async fn process_file() -> Result<(), Error> {
    let mut file = File::open("data.txt").await?;
    let mut buffer = vec![0u8; 1024];  // Owned, moved
    
    process_data(&mut file, &mut buffer).await?;
    // Automatic cleanup via Drop trait
    // But: lifetime annotations can be complex
}

Fidelity’s Vision: Structured Automatic Management

// Fidelity - automatic cleanup with F# elegance
let processFile() = async {
    let! file = File.openAsync "data.txt"    // Compiler knows this needs cleanup
    let buffer = stackalloc<byte> 1024       // Stack allocated, auto-cleaned
    
    let! result = processData file buffer    
    return result
}   // Compiler inserts all cleanup - no explicit disposal needed

// The key: compiler understands resource lifetime from async structure

The Solution: Delimited Continuations with True RAII

The key insight driving Frosty’s design is combining delimited continuations (for explicit async control flow) with true RAII principles (for automatic resource management). This isn’t the .NET pattern of IDisposable - it’s genuine automatic cleanup.

Understanding Delimited Continuations

Delimited continuations provide explicit control over “the rest of the computation”:

// Conceptual transformation
let asyncOperation() = async {
    let! x = fetchData()
    let! y = process x
    return x + y
}

// Becomes explicit continuation structure:
let asyncOperation'() = 
    reset (fun () ->
        shift (fun k1 ->  // k1 is "everything after fetchData"
            fetchData() |> continueWith (fun x ->
                shift (fun k2 ->  // k2 is "everything after process"
                    process x |> continueWith (fun y ->
                        k1 (x + y)
                    )
                )
            )
        )
    )

True RAII: Compiler-Managed Cleanup

Unlike .NET’s IDisposable pattern, true RAII means the compiler automatically inserts cleanup at scope boundaries:

// What you write - no disposal interfaces needed
let processWithResources() = async {
    let! connection = Database.connect "server"
    let! file = File.openWrite "output.txt"
    let buffer = Arena.allocate 4096
    
    let! data = connection.query "SELECT * FROM data"
    do! file.writeAsync buffer data
    
    return data.Length
}   // Compiler ensures all resources cleaned up here

// What the compiler generates (conceptually):
let processWithResources'() = async {
    let! connection = Database.connect "server"
    try
        let! file = File.openWrite "output.txt"
        try
            let buffer = Arena.allocate 4096
            try
                let! data = connection.query "SELECT * FROM data"
                do! file.writeAsync buffer data
                return data.Length
            finally Arena.release buffer
        finally File.close file
    finally Database.disconnect connection
}

The crucial difference from .NET:

  • No IDisposable interfaces - cleanup is structural, not interface-based
  • No forgotten cleanups - compiler tracks all resources
  • No finalizers - everything is deterministic
  • No GC pressure - resources freed immediately at scope exit

Integration with Firefly’s PSG

The explicit control flow of delimited continuations integrates perfectly with Firefly’s Program Hypergraph (PHG). The PSG tracks resource lifetime through the program structure:

// PSG node structure with automatic resource tracking
type PSGNode = {
    Id: NodeId
    Kind: PSGNodeKind
    Range: SourceRange
    Symbol: FSharpSymbol option
    Resources: TrackedResource list  // Compiler-identified resources
}

and TrackedResource = {
    Type: ResourceType
    AcquisitionPoint: NodeId
    CleanupPoint: NodeId  // Compiler-determined, not user-specified
    CleanupAction: CleanupStrategy
}

and CleanupStrategy =
    | StackUnwind          // Automatic for stack resources
    | ArenaReset           // Bulk deallocation
    | CustomCleanup of (unit -> unit)  // For external resources

How the Compiler Identifies Resources

The Firefly compiler recognizes resources through patterns and type analysis:

// Compiler recognizes resource patterns
module ResourceInference =
    let identifyResources (expr: TypedExpr) =
        match expr with
        | Call(method, args) when method.ReturnsResource ->
            // File.open, Database.connect, etc. marked in metadata
            Some (InferredResource method.ResourceType)
            
        | StackAlloc(size) ->
            // Stack allocations are auto-cleaned
            Some (StackResource size)
            
        | ArenaAlloc(arena, size) ->
            // Arena allocations tied to arena lifetime
            Some (ArenaResource(arena, size))
            
        | _ -> None

// Example: compiler metadata for resource-returning functions
[<ReturnsResource(ResourceType.FileHandle, Cleanup.CloseFile)>]
let openFile (path: string) : FileHandle = ...

Memory Management Without Runtime

The combination of delimited continuations and true RAII enables sophisticated memory management without any runtime support:

Stack-Based Async State Machines

// What developers write
let processSequence items = async {
    let mutable sum = 0
    for item in items do
        let! processed = transform item
        sum <- sum + processed
    return sum
}

// Compiler generates fixed-size state machine
type ProcessSequenceStateMachine = struct
    val mutable state: int
    val mutable sum: int
    val mutable currentItem: Item
    val mutable processed: int
    // Total size: 16 bytes, stack allocated
end

// No heap allocation, no GC, predictable memory usage

Arena-Based Bulk Operations

For operations that do need dynamic memory, arenas provide deterministic bulk cleanup:

// Arena allocation with automatic cleanup
let batchProcess records = async {
    let arena = Arena.create 10_000_000  // 10MB arena
    
    // All allocations within this scope use the arena
    let! results = async {
        let accumulator = ResizeArray(records.Length, arena)
        
        for record in records do
            let! processed = transform record
            let entry = arena.allocate<ProcessedEntry>()
            entry.Data <- processed
            accumulator.Add(entry)
            
        return accumulator.ToArray()
    }
    
    return results
}   // Arena automatically reset here - O(1) cleanup

// Compare to .NET where each allocation would need GC

Platform-Aware Compilation

Standard async syntax compiles to platform-specific implementations while maintaining RAII guarantees:

Windows I/O Completion Ports

// What you write
let readFileAsync path = async {
    let! handle = File.openAsync path
    let buffer = stackalloc<byte> 4096
    let! bytesRead = handle.readAsync buffer
    return buffer.Slice(0, bytesRead)
}

// Compiles to IOCP with automatic cleanup
// - OVERLAPPED structure on stack (auto-cleaned)
// - Completion routine registration (auto-unregistered)
// - Buffer on stack (auto-freed)
// - Handle closed at scope exit (compiler-inserted)

Linux io_uring

// Same F# code compiles differently for Linux
// - Submission queue entry (SQE) on stack
// - Ring buffer mapping (auto-unmapped)
// - Automatic cleanup of ring resources
// - No manual resource management needed

Advanced Patterns with Automatic Management

Structured Concurrency

Parallel operations with automatic resource management per branch:

// Each parallel branch gets automatic cleanup
let processInParallel items = async {
    let! results =
        items
        |> List.map (fun item -> async {
            let buffer = Arena.allocate 1024  // Per-branch allocation
            let! result = process buffer item
            return result
        })  // Buffer cleaned up here
        |> Async.Parallel
        
    return List.sum results
}

// Compiler ensures each branch's resources are properly scoped

Railway-Oriented Programming with Guaranteed Cleanup

Error handling that preserves automatic resource management:

// Railway-oriented async with automatic cleanup
type AsyncResult<'T, 'Error> = Async<Result<'T, 'Error>>

let processFileRailway path = asyncResult {
    let! handle = openFileResult path     // Cleaned up on any track
    let! content = readContentResult handle
    let! parsed = parseContentResult content
    return parsed
}   // Handle closed whether success or error - compiler guaranteed

// No need for try-finally or explicit disposal

Cross-Async Resource Sharing

Fidelity enables safe resource sharing across async boundaries:

// Parent async owns the arena
let parentOperation() = async {
    let arena = Arena.create 1_000_000
    
    // Child asyncs can safely use parent's arena
    let! result1 = childOperation1 arena
    let! result2 = childOperation2 arena
    
    return result1 + result2
}   // Arena cleaned up after all children complete

// Safe because compiler tracks arena lifetime through async structure

Integration with Actor Systems

The Olivier actor model extends RAII to concurrent systems:

// Actor with automatic resource management
type DataProcessor() =
    inherit Actor<DataMessage>()
    
    // Resources tied to actor lifetime - no explicit disposal
    let connection = Database.connect "server"
    let cache = Cache.create 10000
    let arena = Arena.create 50_000_000
    
    override this.Receive message = async {
        match message with
        | Query sql ->
            let! results = connection.query sql
            cache.put sql results
            return results
            
        | Process data ->
            use scope = arena.scope()  // Temporary scope
            let! result = processData scope data
            return result
    }
    // When actor terminates, all resources automatically cleaned up
    // No Dispose() method needed!

Compile-Time Guarantees

The combination of delimited continuations and true RAII enables strong compile-time guarantees:

Resource Leak Prevention

// Compiler error: resource escapes its scope
let leakyFunction() =
    let mutable leaked = None
    async {
        let buffer = Arena.allocate 1024
        leaked <- Some buffer  // Error: buffer can't escape async scope
        return ()
    }
    leaked  // Compiler knows this would be invalid

// This is caught at compile time, not runtime!

Bounded Resource Usage

// Compiler computes maximum resource usage
[<MaxStackUsage(4096)>]
[<MaxArenaUsage(1_000_000)>]
let boundedOperation data = async {
    let buffer1 = stackalloc<byte> 2048
    let buffer2 = stackalloc<byte> 2048
    // Compiler: Total stack = 4096 bytes ✓
    
    let arena = Arena.create 1_000_000
    let! result = processWithArena arena data
    return result
}

// Exceeding limits produces compile error, not runtime failure

Why This Matters: The Best of All Worlds

Fidelity’s approach combines advantages from multiple languages while avoiding their drawbacks:

From C++ RAII: Automatic cleanup at scope exit Better because: Type-safe, no undefined behavior, integrated with async

From .NET: Familiar F# syntax and patterns
Better because: Deterministic timing, no GC pauses, safe byrefs

From Rust: Memory safety without GC Better because: No lifetime annotations, natural F# syntax

Unique to Fidelity: Async-aware RAII with compile-time resource tracking

Future Directions

Our research continues in several promising directions:

  • Cross-Process RAII: Extending automatic cleanup across process boundaries
  • Hardware-Accelerated Cleanup: Using memory protection keys for instant arena cleanup
  • Formal Verification: Proving resource safety properties at compile time
  • GPU Resource Management: Extending RAII to GPU memory and kernels

Conclusion

By grounding Frosty in delimited continuations and true RAII principles, we’ve charted a path for F# that transcends the limitations of both systems programming and managed runtimes. This approach provides:

  • Automatic resource management without runtime support
  • Deterministic cleanup without explicit disposal interfaces
  • Safe async programming with compile-time guarantees
  • Platform optimization while maintaining safety
  • Familiar F# syntax hiding the complexity

The key insight is that async computation expressions provide natural scope boundaries for resource lifetime. By making these boundaries explicit through delimited continuations and applying true RAII principles, we achieve something remarkable: the elegance of F# with the performance and predictability of systems programming.

Most importantly, developers write idiomatic F# code while the compiler handles all resource management complexity. No IDisposable, no finalizers, no forgotten cleanup - just async code that works correctly by construction.

This design represents a fundamental advance in language design - proving that functional programming can meet the demanding requirements of systems development without sacrificing elegance or safety. The journey from concept to implementation continues, but the direction is clear: F# can truly become a language for all seasons.

Author
Houston Haynes
date
May 22, 2025
category
Design
reference:

We want to hear from you!

Contact Us