Error Handling

This chapter specifies error handling semantics in Clef, including the relationship between compile-time error propagation through tooling and runtime error handling in compiled applications.

Overview

Clef takes a fundamentally different approach to error handling than managed F#:

AspectManaged F#Clef
Optional valuesoption<'T> with null representation for Nonevoption<'T> (ValueOption) with no null
Failure handlingExceptions (raise, try/with)Result<'T, 'E> with explicit propagation
Null valuesPermitted for reference typesNot permitted; null-free by construction
Runtime type errorsInvalidCastException, NullReferenceExceptionCannot occur; prevented by type system

Design Principle: Errors are values, not control flow. The type system encodes fallibility explicitly, making error handling visible and verifiable at compile time.

The Dual Nature of Error Handling

Clef error handling operates at two distinct levels:

  1. Application Runtime: How compiled Fidelity framework applications handle errors during execution
  2. Tooling Integration: How Clef Compiler Service (CCS) propagates errors through the Language Server Protocol to editors like Lattice

These two domains have different requirements and constraints, but must remain coherent.

Application Runtime Error Handling

The Result Type

The Result<'T, 'E> type is the primary mechanism for representing operations that may fail:

type Result<'T, 'E> =
    | Ok of 'T
    | Error of 'E

Operations that can fail return Result values rather than raising exceptions:

// Managed F# style (NOT used in Clef applications)
let divide x y =
    if y = 0 then raise (DivideByZeroException())
    else x / y

// Clef style
let divide x y : Result<int, DivisionError> =
    if y = 0 then Error DivisionByZero
    else Ok (x / y)

Native Result Operations

The operations below use the canonical Result<'a, 'e> identity and independently quantified NTU payload types:

Result.map : ('a -> 'b) -> Result<'a, 'e> -> Result<'b, 'e>
Result.mapError : ('e -> 'f) -> Result<'a, 'e> -> Result<'a, 'f>
Result.bind : ('a -> Result<'b, 'e>) -> Result<'a, 'e> -> Result<'b, 'e>
Result.defaultValue : 'a -> Result<'a, 'e> -> 'a
Result.defaultWith : ('e -> 'a) -> Result<'a, 'e> -> 'a
Result.iter : ('a -> unit) -> Result<'a, 'e> -> unit
Result.isOk : Result<'a, 'e> -> bool
Result.isError : Result<'a, 'e> -> bool

Explicit type arguments are ordered Result.map<'a, 'b, 'e>, Result.mapError<'a, 'e, 'f>, Result.bind<'a, 'b, 'e>, Result.defaultValue<'a, 'e>, Result.defaultWith<'a, 'e>, Result.iter<'a, 'e>, Result.isOk<'a, 'e> and Result.isError<'a, 'e>.

Their case behavior is:

let map mapper input =
    match input with
    | Ok value -> Ok (mapper value)
    | Error error -> Error error

let mapError mapper input =
    match input with
    | Ok value -> Ok value
    | Error error -> Error (mapper error)

let bind binder input =
    match input with
    | Ok value -> binder value
    | Error error -> Error error

let defaultValue fallback input =
    match input with
    | Ok value -> value
    | Error _ -> fallback

let defaultWith fallback input =
    match input with
    | Ok value -> value
    | Error error -> fallback error

let iter action input =
    match input with
    | Ok value -> action value
    | Error _ -> ()

let isOk input =
    match input with
    | Ok _ -> true
    | Error _ -> false

let isError input =
    match input with
    | Ok _ -> false
    | Error _ -> true

Result operations follow default demand and sharing. The rules below concern ordinary arguments. Direct explicit eager arguments retain their intentional demand at the activated application frontier, including when their values are not selected by the Result case. A demanded case selection requires the input’s tag, not every supplied operand or payload. The callback is used only for the indicated case and only to the extent required by the demanded operation result or explicit iteration effect. Unused callbacks and fallbacks remain deferred, including their initializer effects. Repeated demand through the same operation result shares evaluation. An untouched payload SHALL retain its value, dimensional type and resource identity; changing the other case’s type does not authorize a conversion of that payload. The enclosing Result may require a different realized layout and is not required to retain the same allocation identity. A bind callback’s Result is returned with its case unchanged.

For defaultValue, an Ok input returns the success payload without demanding the fallback; an Error input selects the fallback’s shared computation. For defaultWith, only Error demands the fallback callable and its result, with the error payload as one logical argument. This is an error handler, not the unit thunk used by Option.defaultWith. For a demanded iter, only Ok demands and invokes the action, with the success payload as one logical argument; Error returns unit without forcing the action expression. The action and the operation return unit. Unit-valued and callable payloads retain these logical argument boundaries.

The mapping, binding, defaulting and iteration operations have two operands. Direct applications and pipelines preserve those logical argument associations and shared deferred identities; their written order does not force all operands. When the result type 'a of defaultValue or defaultWith is itself a function, later arguments apply that selected or produced function after the two-operand operation boundary. Later arguments remain deferred until demanded by that returned function. They SHALL NOT be forced before case selection merely because they appear in the same application syntax. Extra application of the unit result of iter is a type error.

Demanded isOk and isError each demand their single Result operand’s case tag, including when applied through a pipe. They SHALL observe only that tag; neither operation demands an unused payload or invokes a callable payload. Constructing the supplied Result does not itself force its payload initializer. Each predicate returns bool, so extra application is a type error. Bare aliases of either predicate SHALL instantiate both payload types independently at each admitted use.

Partial applications SHALL retain the supplied fallback or callback value or shared deferred identity without forcing it at formation, while preserving the identities and obligations of its captured storage. Bare aliases SHALL instantiate the quantified types independently at each admitted use. Binder input and output share the same error type; changing it requires an explicit operation such as mapError.

Baker SHALL elaborate these operations into typed case discrimination and, where required by the operation, case-specific payload extraction, callback application and Result construction before Alex witnesses the graph. Payload reads SHALL occur only within their established case. Joint dimensional, lifetime and resource constraints remain attached to the actual participating values. The operations introduce no new allocation rule: placement follows the settled DU lifetime contract.

Standard Error Types

Clef defines standard error types for common failure modes:

type ArithmeticError =
    | DivisionByZero
    | Overflow
    | Underflow

type IndexError =
    | OutOfBounds of index: int * length: int

type ParseError =
    | InvalidFormat of input: string * expected: string
    | UnexpectedEnd

type IOError =
    | NotFound of path: string
    | PermissionDenied of path: string
    | DeviceError of code: int

The voption Type

For optional values where absence is not an error, voption<'T> (ValueOption) provides a null-free representation:

type voption<'T> =
    | ValueSome of 'T
    | ValueNone

The voption<'T> type has an explicit discriminator with no null representation.

// Looking up a value that may not exist
let tryFind key (map: Map<'K, 'V>) : voption<'V> =
    match Map.tryFind key map with
    | ValueSome v -> ValueSome v
    | ValueNone -> ValueNone

Result Propagation

Clef provides computation expression syntax for Result propagation:

let result {
    let! x = tryParseInt "42"
    let! y = tryParseInt "17"
    return x + y
}

This is equivalent to explicit binding:

match tryParseInt "42" with
| Error e -> Error e
| Ok x ->
    match tryParseInt "17" with
    | Error e -> Error e
    | Ok y -> Ok (x + y)

Try/With Syntax Compatibility

Clef preserves try/with/finally syntax for compatibility with standard F# tooling:

try
    riskyOperation()
with
| :? SomeException as e -> handleError e

Native compilation uses Result values and pattern matching for recoverable failures. Exception-style error handling is diagnosed with CCS8300.

Null-Freedom

Clef is null-free by construction. The following are compile-time errors:

let x : string = null           // ERROR: null literal not available
let y = Unchecked.defaultof<_>  // ERROR for reference types in most contexts
 

This eliminates entire classes of runtime errors:

Managed F# Runtime ErrorClef
NullReferenceExceptionCannot occur
InvalidCastExceptionCannot occur (static typing)
ArrayTypeMismatchExceptionCannot occur (no covariant arrays)

Tooling Integration

CCS Error Propagation

Clef Compiler Service (CCS) must propagate errors through the tooling stack in a format compatible with existing F# tooling infrastructure.

Diagnostic Format

CCS diagnostics follow the F# compiler diagnostic format:

filepath(line,col)-(line,col): severity code: message

For example:

src/Main.clef(12,5)-(12,15): error CCS8010: 'null' is not permitted in Clef; use 'ValueNone' for an absent value

Error Codes

CCS uses error codes in the CCS8xxx range to distinguish native-specific diagnostics:

RangeCategory
CCS8000-CCS8099Type system: identity, measures, seals and ranges, null-freedom (CCS8010, Types and Type Constraints), access kinds
CCS8100-CCS8199Memory management (regions, lifetimes)
CCS8200-CCS8299Platform bindings
CCS8300-CCS8399Effect system
CCS8400-CCS8499Code generation

The CCS code table

Every diagnostic the compiler service reports carries a code in the CCS series. Codes are allocated inside the blocks above and never reassigned. Inherited lexer and parser diagnostics keep their F# number under the CCS prefix (FS0058 becomes CCS0058); the block CCS0000–CCS0999 is reserved for that family.

CodeSeverityMeaning
CCS8000ErrorAn operator’s operand is not numeric (Width Inference, the numeric constraint)
CCS8001ErrorThe kind of an operator’s operands cannot be determined at a binding that is not generalisable
CCS8002ErrorA conversion’s source is not numeric
CCS8003ErrorType mismatch
CCS8004ErrorType constructor arity mismatch
CCS8005ErrorInfinite type (a type variable occurs in its own solution)
CCS8006ErrorTuple mismatch (length or struct kind)
CCS8007ErrorByref kind mismatch
CCS8008ErrorThe constructor is not defined
CCS8009ErrorThe value or constructor is not defined
CCS8010ErrorThe null keyword is not permitted (Types and Type Constraints)
CCS8011ErrorAn integer or dimensioned real whose range is unobservable; for a bare real flowing into a dimension, at the dimensioning seam naming the bare source (Width Inference §6, Numeric Selection §6)
CCS8012ErrorA value’s analysed range is not covered by the boundary’s declared representation (Numeric Selection §5); warning policy cannot authorize failed coverage
CCS8013retired“two seals meet”: there are no seals (NTU Types)
CCS8014InfoA declared boundary representation wider than the range requires; the representation the open argmin would select is named
CCS8015retired“sealed arithmetic may wrap”: arithmetic on analysed ranges never overflows
CCS8016Warning (error under --warnaserror)An analysed range exceeds a higher-provenance claim, a library law’s range or a declaration (Numeric Selection §3.4)
CCS8017retired“a conversion cannot hold the range”: there are no conversions
CCS8018ErrorA literal suffix: every width suffix (L, u, uy, s, n, f), I, and any suffix the language does not have (Width Inference §7)
CCS8019Warning (not promoted by --warnaserror)A compatibility alias uses a width-named spelling (uint32, byte, float32, …) or a width suffix on a literal (0L, 5u, 1.0f, …); the alias denotes the numeric kind with a declared boundary representation
CCS8020–CCS8022ErrorAccess kinds (Access Kinds)
CCS8030–CCS8033ErrorPlatform intrinsics (Platform Bindings)
CCS8040–CCS8050ErrorUnits of measure (Units of Measure): mismatch, no integer solution, not in scope, cyclic abbreviation, variable in a literal, sort mismatch, no dimension, unresolved at a non-generalisable binding, rational exponent, parameterised definition, arity
CCS8060Errorobj is not a Clef type
CCS8061ErrorBoxing is not a Clef operation
CCS8062ErrorDynamic invocation is not a Clef operation
CCS8063ErrorQuote expression patterns are not a Clef construct
CCS8064ErrorInstance member patterns (object expressions) are not a Clef construct
CCS8065ErrorExpression splices (%e, %%e) are not a Clef construct: a quotation is compile-time data read whole (Expressions, Quoted Expressions)
CCS8066ErrorA quotation referenced from executed code: a quotation has no run-time value; reported at each reachable reference, or at the quotation when it stands in executed expression position
CCS8080ErrorA BCL type or namespace is not available in Clef
CCS8081ErrorThe System namespace is not available in Clef
CCS8082ErrorThe Microsoft namespace is not available in Clef
CCS8083ErrorUnchecked.defaultof is not available in Clef
CCS8090ErrorInternal invariant violated in the compiler service (reported, never swallowed)
CCS8091WarningA nullable annotation is ignored; native types are null-free by design
CCS8092WarningType arguments applied to a value that is not a type scheme
CCS8096ErrorInvalid closed callback adapter declaration or application: factory/adapter are not unique immutable module functions, the factory body is not the exact zeroed placeholder, the native listener ABI is missing or incompatible, a handler is not a known closed module function, an adapter captures its handler in a nested closure, or a declared factory escapes without static specialization
CCS8100ErrorRegion mismatch (Memory Regions)
CCS8101ErrorLifetime error
CCS8102ErrorA reference escapes its region
CCS8200ErrorPlatform binding error
CCS8201ErrorUnsupported platform operation
CCS8202ErrorPlatform binding undefined
CCS8203ErrorA site needs a width dimension the platform description does not declare (Platform Bindings, NTU Dimensional Architecture §7.1); never a default
CCS8204ErrorA sealed value’s representation is not offered by the platform description, absent or declared unavailable (Numeric Selection §7)
CCS8205InfoA [platform] key the project file carries that the compiler does not read (word_size): width dimensions and representations come from the platform description
CCS8206ErrorAn element of the platform description the compiler cannot read (a field that is not a literal, an element that is not the record its list is declared over, a Core that is neither Some core nor None), reported at the declaration
CCS8207ErrorAn element of the platform description outside its vocabulary (a capability, family or boundary tag not in its closed set, a width or representation of no bits, a name declared twice, a Register width disagreeing with the word size), reported at the declaration
CCS8208ErrorA second platform description of one form among the platform binding’s sources; the first is read, each other is reported at its declaration
CCS8209ErrorMalformed, inconsistent or ambiguous device-access declaration or plan selection
CCS8210ErrorA used MMIO operation lacks established access evidence, including a contradicted or pending required predicate
CCS8300WarningException-style error handling detected; use the Result-based pattern
CCS8400ErrorCode generation error
CCS8401ErrorUnsupported construct in code generation
CCS8410ErrorA callable aggregate component is assigned ordinary data placement, including an address or integer slot for its code value
CCS8411ErrorA callable aggregate component lacks an established finite alternative family, or its selector domain does not correspond exactly to that family’s alternatives
CCS8412ErrorA callable aggregate selection does not pair its code with the environment instance belonging to the same formation, including unestablished environment absence for a closed component
CCS8413ErrorA callable aggregate slot lacks a required contract identity or combines incompatible contract identities without an established adaptation
CCS8414ErrorRequired callable aggregate formation, tag, selection or dependency evidence is absent, ambiguous or stale at commitment
CCS8415ErrorA commitment relies on an ordinary-execution exclusion whose required activation, use, ownership or dependency evidence is absent, ambiguous or stale
CCS8701–CCS8705ErrorRecord field label resolution (Name Resolution)
CCS8706ErrorA type name in an annotation that resolves to nothing (no abbreviation, definition, primitive or built-in constructor), reported at the annotation; the error type it leaves unifies with anything, so this is the one report of the failure
CCS8710ErrorNull constraint is not a Clef constraint
CCS8711ErrorUnsupported constraint

Callable aggregate and ordinary-demand diagnostics

CCS8410–CCS8415 are allocated for the callable component protocol in Closure Representation §2.4, Discriminated Union Representation §9.1 and FFI Boundary Semantics §3.6. These allocations state the required meanings; they do not establish that a compiler version implements the protocol or emits these codes.

At a source commitment requiring the property, the diagnostic SHALL identify the aggregate construction, assignment, projection or invocation that requires the missing property, or the ordinary binding or callable whose exclusion is being used. It SHALL name the relevant slot and failed premise where applicable. A standalone PSG integrity check SHALL reject invalid rows and identify their participants; it SHALL NOT fabricate a source location when none is available. When source provenance is available, the source diagnostic SHALL retain it.

CCS8410 distinguishes a code value from the selector and environment data which the component protocol permits in data storage. For a callable component with N > 0 alternatives, CCS8411 requires its selector domain to be exactly 0 through N - 1, with one alternative for each selector value. A union case without a callable payload requires no callable selector. CCS8412 applies to each formation even when two formations share an implementation or produce equal values. CCS8413 compares contract identity, not only a function type or equal contract content; an adaptation must be established before the slot is accepted. CCS8414 includes incomplete dependency accounts and duplicated evidence where the protocol requires a unique row. A known lifetime or region violation retains CCS8100–CCS8102; these new codes do not replace those diagnostics.

The absence of a proof that an ordinary body is inactive, or that an ordinary binding is unused, SHALL normally preserve its demand obligations. It SHALL NOT by itself produce CCS8415, force an ordinary initializer, or make a valid inactive callable illegal. CCS8415 applies only when a commitment relies on an exclusion without current evidence establishing it. The exclusion SHALL be withdrawn when its supporting activation, use, capture or ownership facts change; any commitments still required follow their existing diagnostics.

Native callable components in Alex

CodeSeverityMeaning
AX4002ErrorThe callable receiving contract is published, but native callable-component reconstruction is not admitted by the selected witness pathway

AX4002 is allocated for the witness commitment of a native callable component under FFI Boundary §3.6. It applies when the callable receiving contract and code-lifetime evidence are published and the requested reconstruction remains unsupported. Missing or inconsistent source evidence retains its applicable diagnostic.

The diagnostic SHALL identify the aggregate construction, assignment or projection and its resolved slot. It SHALL retain source location information when available and state which witness realization is unsupported. The witness SHALL refuse the operation without emitting operations or binding a result. This allocation does not establish that a compiler version implements native callable-component reconstruction.

LSP Compatibility

CCS implements the Language Server Protocol for editor integration. Key considerations:

  1. Diagnostic Publishing: Errors are published via textDocument/publishDiagnostics in standard LSP format
  2. Code Actions: Quick fixes (e.g., “Insert the explicit conversion”, “Add the measure annotation”) are provided via textDocument/codeAction
  3. Hover Information: Type information displays native types

Lattice Integration Model

The multi-target editor model is well established in the F# ecosystem: Ionide routes a single editing experience across several F# compilation backends.

TargetIntegration Point
.NETFSharp.Compiler.Service
FableFable.Compiler (JavaScript output)
WebSharperWebSharper.Compiler

Lattice, the Clef editor tooling, follows this model:

Lattice ←→ LSP ←→ CCS ←→ Composer Compiler ←→ MLIR/LLVM

Extension Points

CCS provides extension points for Lattice integration:

  1. Project Recognition: .fidproj files identify Clef projects
  2. Target Selection: Lattice can route to CCS when native compilation is detected
  3. Shared Parsing: Syntax parsing uses standard F# lexer/parser for compatibility
  4. Semantic Divergence: Type checking and code generation use native semantics

Compatibility Considerations

To maintain compatibility with the broader F# ecosystem:

  1. Syntax Compatibility: Clef code parses as valid F# syntax
  2. Type Notation: Types are expressed using standard F# type notation
  3. Error Format: Diagnostics follow F# compiler conventions
  4. Incremental Adoption: Projects can mix managed and native targets during migration

Editor Experience

The design-time experience for Clef should be consistent with managed F#:

FeatureBehavior
Syntax highlightingStandard F# highlighting
Error underliningRed squiggles for errors, yellow for warnings
Hover typesShows native type representations
AutocompleteSuggests native library members
Go to definitionNavigates to native library source
Quick fixesOffers native-appropriate fixes

Error Handling Patterns

Railway-Oriented Programming

Clef encourages railway-oriented programming with Result:

let processOrder orderId =
    orderId
    |> validateOrderId
    |> Result.bind fetchOrder
    |> Result.bind validateInventory
    |> Result.bind processPayment
    |> Result.bind shipOrder

Error Aggregation

For operations that may produce multiple errors:

type ValidationErrors = ValidationErrors of ValidationError list

let validateAll validators input =
    validators
    |> List.map (fun v -> v input)
    |> List.fold aggregateErrors (Ok input)

Partial Success

For operations where partial results are meaningful:

type PartialResult<'T, 'E> =
    | Complete of 'T
    | Partial of 'T * 'E list
    | Failed of 'E list

Grammar

result-type := Result < type , type >

voption-type := voption < type >

result-expr :=
    Ok expr
    Error expr

voption-expr :=
    ValueSome expr
    ValueNone

result-bind := let! pattern = expr in expr

result-return := return expr

Diagnostics

Null-freedom is by construction and has exactly one diagnostic, CCS8010 (the null keyword is not permitted, Types and Type Constraints). There is no second null diagnostic: no type “supports null”, no value is uninitialised, and Unchecked.defaultof is BCL surface rejected as such. The codes this chapter contributes:

CodeSeverityMessage
CCS8100ErrorRegion mismatch: a handle of region ‘{r1}’ where region ‘{r2}’ is required (Memory Regions)
CCS8300WarningException-style error handling detected; use the Result-based pattern

A warning is promoted to an error under the --warnaserror policy, the rule every warning in the framework follows (the FPGA timing budget’s CCS0100 is the reference case).