Program Structure and Execution

Program Structure and Execution

Clef Note: Clef programs do not use CLI assemblies. Instead, programs are compiled directly to native binaries from source files, with dependencies resolved at compile time from source packages or pre-compiled native libraries.

Clef programs are composed of an ordered sequence of signature (.fsi) files and implementation (.fs) files, plus any library dependencies specified in the project file (.fidproj). Script files (.fsx) are supported for development and tooling but are not part of native compilation.

implementation-file :=
    namespace-decl-group ... namespace-decl-group
    named-module
    anonynmous-module

script-file := implementation-file  -- script file, additional directives allowed

signature-file :=
    namespace-decl-group-signature ... namespace-decl-group-signature
    anonynmous-module-signature
    named-module-signature

named-module :=
    module long-ident module-elems

anonymous-module :=
    module-elems

named-module-signature :=
    module long-ident module-signature-elements

anonymous-module-signature :=
    module-signature-elements

script-fragment :=
    module-elems                    -- interactively entered code fragment

A sequence of implementation and signature files is checked as follows.

  1. Form an initial environment sig-env0 and impl-env0 by adding all library dependencies to the environment in the order specified in the project file. This means the following procedure is applied for each dependency:

    • Add the top-level types, modules, and namespaces to the environment.
    • For each AutoOpen attribute in the library, find the types, modules, and namespaces that the attribute references and add these to the environment.

    Clef Note: The native standard library is automatically included and provides the core types (string, option, int, etc.) with native semantics. See Native Type Mappings.

    The resulting environment becomes the active environment for the first file to be processed.

  2. For each file:

    • If the ith file is a signature file file.fsi:

      a. Check it against the current signature environment sig-envi1, which generates the signature Sigfile for the current file.

      b. Add Sigfile to sig-envi-1 to produce sig-envi to make it available for use in later signature files.

      The processing of the signature file has no effect on the implementation environment, so impl-envi is identical to impl-envi-1.

    • If the file is an implementation file file.fs, check it against the environment impl-envi-1, which gives elaborated namespace declaration groups Implfile.

      a. If a corresponding signature Sigfile exists, check Implfile against Sigfile during this process (§). Then add Sigfile to impl-envi-1 to produce impl-envi. This step makes the signature-constrained view of the implementation file available for use in later implementation files. The processing of the implementation file has no effect on the signature environment, so sig-envi is identical to sig-envi-1.

      b. If the implementation file has no signature file, add Implfile to both sig-envi-1 and impl-envi-1, to produce sig-envi and impl-envi. This makes the contents of the implementation available for use in both later signature and implementation files.

The signature file for a particular implementation must occur before the implementation file in the compilation order. For every signature file, a corresponding implementation file must occur after the file in the compilation order. Script files may not have signatures.

Implementation Files

Implementation files consist of one or more namespace declaration groups. For example:

namespace MyCompany.MyOtherLibrary

    type MyType() =
        let x = 1
        member v.P = x + 2

    module MyInnerModule =
        let myValue = 1

namespace MyCompany. MyOtherLibrary.Collections

    type MyCollection(x : int) =
        member v.P = x

An implementation file that begins with a module declaration defines a single namespace declaration group with one module. For example:

module MyCompany.MyLibrary.MyModule

let x = 1

is equivalent to:

namespace MyCompany.MyLibrary

module MyModule =
    let x = 1

The final identifier in the long-ident that follows the module keyword is interpreted as the module name, and the preceding identifiers are interpreted as the namespace.

Anonymous implementation files do not have either a leading module or namespace declaration. Only the scripts and the last file within an implementation group for an executable image (.exe) may be anonymous. An anonymous implementation file contains module definitions that are implicitly placed in a module. The name of the module is generated from the name of the source file by capitalizing the first letter and removing the filename extensionIf the filename contains characters that are not valid in an F# identifier, the resulting module name is unusable and a warning occurs.

Given an initial environment env0, an implementation file is checked as follows:

  • Create a new constraint solving context.
  • Check the namespace declaration groups in the file against the existing environment envi-1 and incrementally add them to the environment (§) to create a new environment envi.
  • Apply default solutions to any remaining type inference variables that include default constraints. The defaults are applied in the order that the type variables appear in the type- annotated text of the checked namespace declaration groups.
  • Check the inferred signature of the implementation file against any required signature by using Signature Conformance (§). The resulting signature of an implementation file is the required signature, if it is present; otherwise it is the inferred signature.
  • Report a “value restriction” error if the resulting signature of any item that is not a member, constructor, function, or type function contains any free inference type variables.
  • Choose solutions for any remaining type inference variables in the elaborated form of an expression. Process any remaining type variables in the elaborated form from left-to-right to find a minimal type solution that is consistent with constraints on the type variable. If no unique minimal solution exists for a type variable, report an error.

The result of checking an implementation file is a set of elaborated namespace declaration groups.

Signature Files

Signature files specify the functionality that is implemented by a corresponding implementation file. Each signature file contains a sequence of namespace-decl-group-signature elements. The inclusion of a signature file in compilation implicitly applies that signature type to the contents of a corresponding implementation file.

Anonymous signature files do not have either a leading module or namespace declaration. Anonymous signature files contain module-elems that are implicitly placed in a module. The name of the module is generated from the name of the source file by capitalizing the first letter and removing the filename extension. If the filename contains characters that are not valid in an F# identifier, the resulting module name is unusable and a warning occurs.

Given an initial environment env , a signature file is checked as follows:

  • Create a new constraint solving context.
  • Check each namespace-decl-group-signaturei in envi-1 and add the result to that environment to create a new environment envi.

The result of checking a signature file is a set of elaborated namespace declaration group types.

Script Files

Clef Note: Script files (.fsx, .fsscript) are primarily used for development tooling and F# Interactive. They are not directly compiled to native binaries by the Firefly compiler. For native compilation, use implementation files (.fs) organized via a .fidproj project file.

Script files have the .fsx or .fsscript filename extension. They are processed for development scenarios with the following characteristics:

  • Side effects from scripts are executed immediately in the interactive environment.
  • Script files may add other signature, implementation, and script files to the list of sources by using the #load directive. Files are compiled in the same order that was passed to the compiler, except that each script is searched for #load directives and the loaded files are placed before the script, in the order they appear in the script. If a filename appears in more than one #load directive, the file is placed in the list only once, at the position it first appeared.
  • Script files may have #nowarn directives, which disable a warning for the entire compilation.

The Firefly compiler defines the FIDELITY compilation symbol for native compilation. The COMPILED symbol is also defined for compatibility.

Script files may not have corresponding signature files.

Compiler Directives

Compiler directives are declarations in non-nested modules or namespace declaration groups in the following form:

# id string ... string

The lexical preprocessor directives #if, #else, #endif and #indent "off" are similar to compiler directives. For details on #if, #else, #endif, see §. The #indent "off" directive is described in §.

The following directives are valid in all files:

DirectiveExampleShort Description
#nowarn#nowarn "54"For signature (.fsi) files and implementation (.fs) files, turns off warnings within this lexical scope. For script (.fsx or .fsscript) files, turns off warnings globally.

Clef Note: The #r directive for referencing assemblies is not applicable to native compilation. Dependencies are specified in the .fidproj project file. The following script directives are supported only in F# Interactive tooling, not in native compilation:

DirectiveExampleShort Description
#load#load "core.fsi" "core.fs"Loads a set of signature and implementation files into the script execution engine.
#time#time "on"Enables or disables the display of performance information.
#help#helpAsks the script execution environment for help.
#quit#quitRequests the script execution environment to halt execution and exit.

Program Execution

Clef Note: Execution of Clef code occurs as a standalone native binary, not within a CLI runtime. There is no assembly loading, no JIT compilation, and no garbage collector. Memory management is deterministic and controlled by the compiler.

Execution of Clef code begins when the native binary is loaded by the operating system. During execution, the program can use the functions, values, static members, and object constructors that the compiled modules define.

Execution of Static Initializers

Each implementation file involves a static initializer. In Clef, static initialization is deterministic and occurs at program startup:

  • For executables with an explicit entry point function, the static initializers for all files are executed in compilation order before the entry point function is called.
  • For executables with an implicit entry point, the static initializer for the last file is the body of the implicit entry point function.

Clef Note: Static initialization is eager and deterministic. All module-level bindings with observable initialization are evaluated at program startup, in compilation order. This provides predictable behavior essential for embedded and real-time systems.

At startup, the static initializer evaluates, in order, the definitions in each file that have observable initialization. Definitions with observable initialization in nested modules and types are included in the static initializer for the overall file.

All definitions have observable initialization except for the following definitions in modules:

  • Function definitions
  • Type function definitions
  • Literal definitions
  • Value definitions that are generalized to have one or more type variables
  • Non-mutable values that are bound to an initialization constant expression, which is an expression whose elaborated form is one of the following:
    • A simple constant expression.
    • A use of the sizeof<_> operator or the defaultof<_> operator from Unchecked.
    • A let expression where the constituent expressions are initialization constant expressions.
    • A match expression where the input is an initialization constant expression, each case is a test against a constant, and each target is an initialization constant expression.
    • A use of one of the unary or binary operators =, <>, <, >, <=, >=, +, -, *, <<<, >>>, |||, &&&, ^^^, ~~~, enum<_>, not, compare, prefix -, and prefix + on one or two arguments, respectively. The arguments themselves must be initialization constant expressions, but cannot be operations on decimals or strings.
    • A use of a [<Literal>] value.
    • A use of a case from an enumeration type.
    • A use of a value that is defined in the same compilation unit and does not have observable initialization.

If the execution environment supports concurrent execution of multiple threads, each static initializer runs as a mutual exclusion region. A static initializer runs only once, on the first thread that acquires entry to the mutual exclusion region.

For example, if the program accesses data in this example, the static initializer runs and the program prints “hello”:

module LibraryModule
printfn "hello"
let data = Map.empty<int, int>

All of the following represent definitions that do not have observable initialization because they are initialization constant expressions:

let x = DayOfWeek.Friday
let x = 1.0
let x = "two"
let x = enum<DayOfWeek>(0)
let x = 1 + 1
let x : int list = []
let x : int option = None
let x = compare 1 1
let x = match true with true -> 1 | false -> 2
let x = true && true
let x = 42 >>> 2
let x = Unchecked.defaultof<int>
let x = Unchecked.defaultof<string>
let x = sizeof<int>

Explicit Entry Point

The last file that is specified in the compilation order for an executable file may contain an explicit entry point. The entry point is indicated by annotating a function in a module with EntryPoint attribute:

  • The EntryPoint attribute applies only to a “let”-bound function in a module. The function cannot be a member.
  • This attribute can apply to only one function, and the function must be the last declaration in the last file processed. The function may be in a nested module.
  • The function is asserted to have type array<string> -> int before type checking. If the assertion fails, an error occurs.
  • At startup, the entry point is passed one argument: an array that contains the command-line arguments passed to the program (excluding the program name).

The function becomes the entry point to the program. At startup, Clef executes all static initializers in compilation order, then evaluates the body of the entry point function.

Clef Note: The entry point function’s return value becomes the process exit code. A return value of 0 indicates success; non-zero values indicate errors. For freestanding (no-OS) targets, the return value may be ignored or handled by the runtime stub.

Entry Point Modes

Clef supports multiple entry point modes, specified via output_kind in the project file:

ModeEntry Symbollibc RequiredDescription
ConsolemainYesStandard mode; libc provides _start which calls user’s main
Freestanding_startNoBare metal; compiler generates _start wrapper
LibraryNoneOptionalShared library with exported symbols

Freestanding Entry Point Generation

For freestanding builds (output_kind = "freestanding"), the compiler generates a _start wrapper function that:

  1. Creates an empty string array (F# convention; full argc/argv conversion is future work)
  2. Calls the user’s main function with this argument
  3. Calls Sys.exit with the return value

The generated _start has F# type unit -> unit:

  • Takes no parameters (startup context comes from the platform)
  • Returns unit (because Sys.exit never returns; the syscall terminates the process)
_start : unit -> unit
    argv ← Sys.emptyStringArray()     // Empty string array
    result ← main(argv)               // Call F# main
    Sys.exit(result)                  // Terminate with exit code (never returns)

Implementation Note: Sys.exit has type int -> unit. Although the syscall never returns, the binding honors the type contract by emitting an unreachable unit return value. This maintains type consistency throughout the compilation pipeline.

Console Mode (libc)

For console builds (output_kind = "console"), the platform’s libc provides _start, which:

  1. Initializes the C runtime
  2. Converts argc/argv from the stack
  3. Calls the user’s main symbol

The compiler generates main with a signature compatible with the platform’s C ABI. The F# type array<string> -> int maps to the platform-appropriate C signature.

See Platform Bindings for platform descriptor conventions.