Program Initialization and Shutdown

Sec keeps program lifecycle explicit. Importing a module does not execute code, and a module does not receive a hidden initialization function merely because it contains declarations. Runtime initialization happens because the program explicitly requests it.

The same principle applies at the other end of the program. Normal scope exit and a normal return from the executable entry point participate in ordinary Sec cleanup. Abrupt target termination, a raw non-returning platform exit or an aborting panic must not be described as if they had executed cleanup that never ran.

fn main() int {
    match StartApplication() {
        Ok(app) => return Run(app)
        Err(error) => return 1
    }
}
Core rule: Program startup and shutdown must not depend on hidden module constructors, import side effects or implicit global finalizer registries. Runtime work is explicit in the program or explicitly owned by the selected platform.

Purpose

Program initialization is broader than constructing one value. It includes the path from the target's machine/runtime entry into the Sec executable, explicit setup of application services, establishment of long-lived ownership and the corresponding orderly shutdown path.

This chapter distinguishes:

  • platform bootstrap before Sec application code begins;
  • the executable's Sec entry point;
  • explicit library or subsystem initialization;
  • construction of individual values through init and new;
  • static storage that exists for the program lifetime;
  • normal cleanup and graceful termination;
  • abrupt termination paths where cleanup cannot be guaranteed.

Explicit Lifecycle Principle

Sec deliberately avoids lifecycle behavior that is triggered merely by loading, importing or discovering source code.

In particular:

  • imports make symbols available;
  • imports do not execute user code;
  • there is no implicit per-module init function;
  • there is no import-order runtime initialization chain;
  • initialization APIs are called because source code calls them;
  • shutdown APIs are called because source code or ordinary ownership cleanup reaches them.

This makes startup visible in code review and keeps dependency order explicit rather than hiding it in linker or loader behavior.

Executable Entry Point

A Sec executable has a main entry point in its executable module. main is an ordinary Sec function from the language's point of view: it has lexical scope, local ownership, defer, error handling and ordinary destruction behavior.

fn main() int {
    return 0
}

The selected target and executable ABI determine how the platform transfers control into the Sec entry point and how the returned status is represented externally. Those ABI details do not create hidden Sec module initialization semantics.

Rule: The platform may provide bootstrap code around main, but user-visible application initialization remains explicit.

Executable Modules

Project layout identifies executable targets separately from reusable modules. A directory under cmd represents one executable target.

cmd/
    server/
        main.sec
    worker/
        main.sec

Each executable has its own entry point and therefore its own explicit initialization sequence. There is no special generic main module shared by all executables.

Platform Bootstrap

Before Sec application code can run, the selected target may require platform work: creating a stack, establishing runtime support, mapping required memory, preparing an ABI environment or entering the correct execution mode.

This work belongs to the platform/runtime implementation selected by the CompilationPlan. It is not equivalent to module initialization and it must not make an imported application module execute arbitrary code.

machine / loader / firmware entry
              |
              v
       platform bootstrap
              |
              v
        Sec entry point
              |
              v
 explicit application setup

Hosted, RTOS and BareMetal profiles may realize the bootstrap differently while the source-level rule remains the same: platform preparation is separate from explicit application initialization.

Imports Do Not Initialize

An import changes name availability, not runtime state.

import "database"
import "log"
import "net/http"

None of these imports opens a database, starts logging, binds a socket, allocates an HTTP server or registers a shutdown callback merely because the import exists.

If a module needs setup, the program calls an API that performs the setup explicitly.

let database := try database.Open(config.Database)
let server := try http.Listen(config.Address)

Explicit Runtime Initialization

Runtime initialization is ordinary Sec code. It may allocate, perform I/O, access hardware, return typed errors, construct owned resources and register ordinary cleanup through scope ownership or defer.

fn StartApplication() Result[Application, StartError] {
    let config := try LoadConfiguration()
    let database := try Database.Open(config.Database)
    let server := try Server.Open(config.ListenAddress)

    return Ok(Application {
        Config: config,
        Database: database,
        Server: server,
    })
}

If initialization fails part-way through, already constructed owned values follow the normal cleanup rules. A separate global rollback mechanism is not needed merely because the code happens to run during startup.

Initialization Functions

A library may expose a named setup function when that API makes sense. Such a function is an ordinary function, not a magic language hook.

try sqlite.Init()

Another library may instead return an owned value:

let database := try Database.Open(settings)

The language does not require every library to use the name Init, and it does not call such functions automatically. APIs should choose names that describe what they actually establish.

Object init vs Program Initialization

The lifecycle member init defined inside an impl constructs one value. It is not a program-start hook.

impl Connection {
    init(address: Address) ConnectError {
        ...
    }
}

Using that initializer:

let connection := try new Connection(address)

constructs one Connection. The compiler does not scan all types for init members and invoke them at process startup.

Rule: init belongs to value construction. Program initialization is ordinary explicit control flow from the executable entry point.

Static and Global State

Type-owned static members may exist independently of instances:

impl Metrics {
    static let MaximumSeries: uint := 1024
    static let mut DroppedSamples: uint64 := 0
}

Static storage is not the same thing as hidden runtime initialization. A static value whose representation can be established directly does not require a user-visible startup callback merely to exist.

Resource-owning global state is more dangerous because it would require implicit construction and process-exit destruction. Sec's initial model therefore avoids relying on hidden global constructors or a hidden global destruction registry.

Static Initializers

A static initializer should describe state that can be established without inventing an invisible runtime initialization order.

impl Limits {
    static let DefaultPort: uint16 := 8080
}

When setup requires runtime work such as allocation, file access, socket creation, device probing or foreign initialization, put that work in explicit startup code.

fn main() int {
    match Runtime.Open() {
        Ok(runtime) => return Run(runtime)
        Err(error) => return 1
    }
}

This keeps failure handling and cleanup visible instead of hiding them behind static declaration order.

Initialization Order

Because imports do not execute code, Sec does not need a language-wide rule such as “initialize imported modules in dependency order”. Application initialization order is the order expressed by ordinary source control flow.

fn Start() Result[Application, StartError] {
    let logging := try Logging.Open()
    let database := try Database.Open()
    let server := try Server.Open(database)

    return Ok(Application {
        Logging: logging,
        Database: database,
        Server: server,
    })
}

Here the order is unambiguous because it is written directly in the function. Dependency order, failure order and ownership order all follow the same visible code.

Initialization Failure

Startup code uses ordinary Result and try behavior. If a later initialization step fails, earlier successfully initialized owned values are cleaned up according to their normal scope and destruction rules.

fn Start() Result[Application, StartError] {
    let database := try Database.Open()
    let cache := try Cache.Open()
    let server := try Server.Open()

    return Ok(Application {
        Database: database,
        Cache: cache,
        Server: server,
    })
}

If Cache.Open() fails, database remains the caller's owned startup temporary and is cleaned up before the error leaves the function. If Server.Open() fails, both cache and database are cleaned up in the ordinary reverse ownership order.

Ownership of Runtime State

Long-lived program state should have a visible owner.

type Application struct {
    Database: Database,
    Server: Server,
    Metrics: Metrics,
}

Returning one aggregate from startup often makes shutdown simple: the entry point or top-level runtime loop owns the application, and ordinary destruction releases its fields when that owner ends.

This is preferable to distributing independent hidden global owners across imported modules, because ownership, shutdown order and failure cleanup remain statically visible.

defer During Program Lifetime

defer is ordinary function cleanup and can be useful in startup and entry functions when an explicit action must occur when that scope exits.

fn RunService() Result[void, ServiceError] {
    let service := try Service.Open()

    defer {
        service.FlushMetrics()
    }

    return service.Run()
}

The deferred block does not become a process-global shutdown hook. It belongs to the lexical function execution that registered it and runs only on cleanup paths for which the language guarantees defer execution.

Graceful Shutdown

Graceful shutdown is the deliberate transition from running application state to a normal Sec control-flow exit where owned values can be released in a defined order.

A typical shape is:

  1. stop accepting new work;
  2. request cancellation or completion of owned concurrent work;
  3. join or otherwise resolve lifecycle-owned tasks, threads or processes;
  4. perform explicit fallible close operations whose errors matter;
  5. leave scopes normally so remaining infallible destruction runs;
  6. return from main.

The exact application protocol is domain-specific, but the language does not hide it in a global finalizer phase.

Returning from main

A normal return from main is a normal Sec function return. Before control leaves the function, ordinary cleanup for live owned locals and registered defer operations is performed according to the cleanup rules.

fn main() int {
    match StartApplication() {
        Ok(application) => return Run(application)
        Err(error) => return 1
    }
}

The returned status is prepared before the function's remaining local cleanup is completed, just like any other return value.

Explicit Close Before Shutdown

Destruction itself cannot report a recoverable cleanup error to a caller after the value's lifetime has ended. Resources for which closing errors matter should expose an explicit ordinary close operation.

let file := try File.Open(path)

// use file

try file.Close()

A type may still provide infallible or best-effort automatic destruction as a safety fallback, but semantically meaningful close errors must remain visible in ordinary program control flow.

Tasks, Threads and Processes

Program shutdown must not silently discard unresolved ownership merely because the executable is about to terminate. Concurrent and process lifecycle values follow their own ownership rules.

For example, an unresolved process owner cannot simply fall out of scope when the process model requires an explicit join, detach or transfer. Likewise, later task and thread chapters define which execution owners must be completed, cancelled, joined or otherwise resolved before their owning scope ends.

Graceful shutdown should therefore resolve execution ownership before final top-level scope exit rather than relying on the operating system to discard everything.

Raw Exit and Non-Returning Calls

A raw platform process-exit syscall or equivalent non-returning primitive is not a normal Sec return. The compiler must not claim that cleanup after that call executes.

unsafe {
    PlatformRawExit(1)
}

// no cleanup after the non-returning exit can be promised

Any cleanup required before such an operation must already have executed explicitly. This is one reason ordinary return from main is the preferred graceful termination path on hosted targets.

Panic and Abnormal Termination

Panic policy is selected by the target profile. Until a target explicitly provides a defined unwinding model, code must not assume that panic runs every outer cleanup or destructor.

Likewise, external termination such as power loss, hardware reset, operating-system kill, process crash or machine shutdown cannot guarantee that ordinary Sec cleanup finishes.

Rule: Deterministic destruction defines normal language-controlled lifetime endings. It is not a promise that all resources are released after arbitrary external or aborting termination.

No Hidden Global Finalizers

Sec does not need a hidden process-wide list of destructors merely because static declarations exist. The initial model avoids global owned values whose correctness depends on process-exit finalization.

Compile-time constants and immutable static data that require no runtime destruction are straightforward. Runtime resources that require cleanup should normally be owned by explicit application state created during startup.

fn main() int {
    match Application.Open() {
        Ok(application) => return application.Run()
        Err(error) => return 1
    }
}

When application ends, its owned fields follow ordinary destruction. No separate global finalizer registry is required.

Bare Metal and Hardware Targets

Bare-metal startup often requires more platform work before ordinary Sec code can execute: establishing stack state, memory regions, interrupt metadata, runtime services or device access. Those requirements belong to the selected platform and target profile.

The language still avoids hidden imported-module initialization. Once control reaches the Sec application entry point, application hardware configuration remains explicit.

fn InitializeHardware() Result[void, HardwareError] {
    try Clock.Configure()
    try Board.InitializePeripherals()
    return Ok()
}

fn main() int {
    match InitializeHardware() {
        Ok() => {
            RunControlLoop()
            return 0
        }
        Err(error) => return 1
    }
}

Hardware reset, power loss or watchdog reset may bypass graceful shutdown entirely. Code must not design device correctness around an assumption that process-exit cleanup always runs on such targets.

Hosted Targets

A hosted target normally begins through an operating-system or loader ABI. The Sec platform layer establishes the environment required by the compiled program and then transfers control to the executable entry point.

Hosted support does not change the source rule: imports remain non-executing, application initialization remains explicit and normal return remains the cleanest language-controlled shutdown path.

RTOS Targets

An RTOS target may require scheduler or kernel integration before application code begins. Whether the Sec entry point runs as an RTOS task, a bootstrap function or another target-defined environment is a platform implementation detail described by the active target profile.

The existence of an RTOS does not authorize hidden module initialization or hidden shutdown callbacks. Application-owned services and execution resources remain subject to explicit ownership and lifecycle rules.

FFI and Foreign Runtime Startup

A Sec executable may depend on foreign runtimes or libraries that require explicit initialization or shutdown. That requirement belongs in an ordinary Sec wrapper API.

fn StartForeignRuntime() Result[ForeignRuntime, ForeignError] {
    unsafe {
        let status := foreign_runtime_init()
        return DecodeRuntime(status)
    }
}

The wrapper defines error, ownership and cleanup semantics. The compiler must not invent lifecycle behavior merely because an extern declaration exists.

Effects and Static Analysis

Startup and shutdown code are not exempt from Sec's effect and safety analyses. Initialization may allocate, perform I/O, access volatile hardware, block or call unsafe/foreign operations only when those operations are valid in the active context and target profile.

Likewise, cleanup participates in ownership, borrow, race, deadlock and execution lifecycle analysis. A function being “startup code” does not grant extra authority.

Diagnostics and Tooling

Useful diagnostics include:

  • executable target has no valid main entry point;
  • more than one entry point is visible for one executable target;
  • runtime initialization result is ignored;
  • static initialization requires unsupported hidden runtime work;
  • global owned state would require unsupported process-exit destruction;
  • unresolved task, thread or process ownership remains at program exit;
  • cleanup is placed after a known non-returning raw exit operation;
  • startup operation requires a target capability that the active profile does not provide.

Tooling should show the selected executable entry point and target profile explicitly. It should not infer startup behavior from import order.

Complete Hosted Example

type Application struct {
    Database: Database,
    Server: Server,
}

fn StartApplication() Result[Application, StartError] {
    let config := try LoadConfiguration()
    let database := try Database.Open(config.Database)
    let server := try Server.Open(config.ListenAddress)

    return Ok(Application {
        Database: database,
        Server: server,
    })
}

fn Run(application: Application) int {
    return application.Server.Run()
}

fn main() int {
    match StartApplication() {
        Ok(application) => return Run(application)
        Err(error) => return 1
    }
}

Startup order is visible. Failure uses ordinary typed errors. Ownership of the database and server moves into Application. Normal return follows normal cleanup. No import executes startup code and no hidden finalizer registry is needed.

Bare-Metal Style Example

fn ConfigureBoard() Result[void, BoardError] {
    try Clock.Initialize()
    try GPIO.Initialize()
    try Timer.Initialize()
    return Ok()
}

fn main() int {
    match ConfigureBoard() {
        Ok() => {
            while true {
                RunControlStep()
            }
        }
        Err(error) => return 1
    }
}

The platform may perform machine bootstrap before main, but board setup requested by the application is explicit. If the program is intentionally non-terminating, normal process shutdown is simply not part of its ordinary path.

Current Status and Boundaries

The core lifecycle rules used by this chapter are already established across Sec's module, project, implementation, destruction and platform models:

  • imports do not execute code;
  • initialization is explicit;
  • executables have an explicit entry point;
  • type init is value construction, not module startup;
  • normal return participates in ordinary cleanup;
  • raw non-returning exit does not imply caller cleanup;
  • global owned destruction is not based on a hidden runtime registry.

Exact machine bootstrap sequences, loader glue, RTOS entry adaptation and external process-status ABI mapping are platform/backend responsibilities. They must preserve the source-level rules above but may differ between targets.

Best Practices

  • Keep application initialization visible from the executable entry point.
  • Do not rely on imports to establish runtime state.
  • Prefer returning owned application state from startup rather than filling hidden globals.
  • Use ordinary Result and try for startup failures.
  • Let normal ownership clean up partially completed initialization.
  • Use type init only for constructing one value; do not treat it as a program hook.
  • Keep static initialization free of hidden fallible runtime work.
  • Resolve owned tasks, threads and processes before top-level graceful exit.
  • Call explicit fallible Close-style APIs before destruction when close errors matter.
  • Prefer normal return from main over a raw process-exit syscall when cleanup matters.
  • Do not assume panic, power loss, reset or external termination runs ordinary cleanup.
  • Keep platform bootstrap separate from application initialization.

Summary

  • Sec program lifecycle is explicit by design.
  • Imports make symbols available and never execute module initialization code.
  • There are no implicit per-module initialization functions.
  • Executable targets enter through an explicit main function.
  • Platform bootstrap may prepare the machine/runtime before main without becoming module initialization.
  • Runtime library and subsystem setup is ordinary explicitly called Sec code.
  • Type init constructs one value and is unrelated to automatic program startup.
  • Static storage does not justify hidden runtime constructor chains.
  • Initialization order is the visible source execution order.
  • Startup failure uses ordinary Result, try, ownership and cleanup semantics.
  • Long-lived runtime resources should have explicit owners.
  • defer is lexical cleanup, not a process-global shutdown hook.
  • Graceful shutdown resolves owned execution and resources before ordinary top-level exit.
  • Returning from main executes normal Sec cleanup.
  • Raw non-returning exit operations do not promise cleanup after the exit call.
  • Aborting panic, power loss, reset and external termination cannot guarantee ordinary cleanup.
  • Sec does not require a hidden global finalizer registry.
  • Hosted, RTOS and BareMetal targets may use different bootstrap mechanisms while preserving the same source-level lifecycle rules.