Conditional Compilation and Build Selection

Sec performs conditional compilation by selecting source declarations for one concrete CompilationPlan.

The two source-level selection mechanisms are:

@target(...)
@when(...)

@target selects according to target identity. @when selects according to typed compile-time program configuration.

Core rule: Conditional compilation selects declarations before active semantic analysis. It is not runtime branching and it is not textual preprocessing.

The Compilation-Plan Model

Every concrete build resolves a compilation plan containing facts such as:

  • logical project and target identity;
  • selected variant;
  • operating system;
  • architecture;
  • ABI;
  • CPU and CPU features;
  • device and board where applicable;
  • selected profile;
  • compiler options;
  • typed compile-time parameters.

Source selection uses the target identity and compile-time parameter portions of that plan.

After selection, the compiler constructs the active source graph and symbol set for that concrete plan.

Sec Has No Textual Preprocessor

Sec 0.1 does not provide C-style:

#define
#ifdef
#ifndef
-DNAME
text substitution
token concatenation
conditional lexical regions

Conditional compilation works on parsed declarations rather than raw source text.

This keeps syntax, formatting, source mapping and editor tooling available for all source, including source that is inactive in the current compilation plan.

Selection Happens Before Active Semantic Analysis

The conceptual order is:

parse source
    -> evaluate file-level target selection
    -> evaluate top-level @target and @when
    -> construct active source/symbol set
    -> validate declaration variants
    -> active semantic analysis
    -> lowering

Excluded source therefore does not participate in the current active symbol table, ordinary active Sema, Semantic IR or lowering.

Selection must occur early enough that mutually exclusive declarations can share one logical symbol identity.

Target Selection with @target

@target selects source according to target identity:

@target(os: "linux", arch: "amd64")
fn RawWrite(
    data: ref byte[],
) Result[int, WriteError] {
    ...
}

The same attribute form is used for complete-file and single-declaration selection. Position determines which meaning applies.

File-Level @target

When @target is the first source-bearing line in a file, it selects the complete file:

@target(os: "linux", arch: "amd64")

module amd64

fn RawSystemCall(...) int64 {
    ...
}

Comments and an allowed byte-order marker may precede the attribute.

To retain file-level meaning, the attribute must appear before:

  • the module declaration;
  • imports;
  • types;
  • functions;
  • variables;
  • any other non-comment source statement.

When the target selectors do not match, the complete file is excluded from the active plan.

Statement-Level @target

When @target is not the first source-bearing line, it selects only the next allowed top-level statement:

@target(os: "linux")
import "platform/linux"
@target(device: "controller-a")
@address(Peripheral.GPIOA)
let mut GPIOA: GPIORegisters
@target(os: "linux", arch: "amd64")
fn RawWrite(...) Result[int, WriteError] {
    ...
}

Allowed statement categories include:

  • imports;
  • type, enum, union, register and unit declarations;
  • interfaces;
  • impl blocks;
  • functions;
  • extern declarations;
  • module-scope variables;
  • static declarations;
  • other top-level declaration forms allowed by the grammar.

@target does not attach to the module declaration itself. Module identity remains stable across target variants.

Target Selectors

Sec 0.1 defines these target selectors:

os
arch
cpu
device
board

Examples:

@target(os: "linux")
@target(arch: "amd64")
@target(cpu: "cortex-m4")
@target(device: "stm32f407vg")
@target(board: "controller-rev-b")

Several selectors may be combined:

@target(
    os: "linux",
    arch: "arm64",
    cpu: "cortex-a72",
    board: "raspberry-pi-4",
)

Unknown selector names are errors. The compiler does not accept a selector merely because a future version may define it.

os, arch, cpu, device and board

SelectorMeaning
osOperating system or freestanding environment family
archInstruction-set architecture or architecture family
cpuProcessor core or processor implementation
deviceComplete MCU, SoC or other target device
boardConcrete board or hardware product containing the device and external components

For example:

arch   = "arm32"
cpu    = "cortex-m4"
device = "stm32f407vg"
board  = "controller-rev-b"

A device is not interchangeable with a CPU. Two devices using the same CPU core may still have different memory maps, peripheral blocks, DMA channels, interrupt vectors, clock controllers and pin multiplexing.

Device-specific storage or interrupt bindings should therefore normally select by device or board, not merely by cpu.

Target Selectors Combine with Logical AND

At least one selector is required. Omitted selectors impose no restriction.

All supplied selectors must match:

@target(os: "linux", arch: "amd64")

means:

os == "linux" AND arch == "amd64"

There is no implicit OR between selector arguments. Use separate mutually exclusive declarations when one logical symbol has several different target implementations.

Target Selector Values

String literals are accepted for the initial target selectors.

Matching uses canonical target-name normalization defined by target knowledge.

The compiler diagnoses cases such as:

  • unknown target value;
  • CPU incompatible with the selected architecture;
  • device incompatible with the selected CPU;
  • board incompatible with the selected device;
  • contradictory selectors.

Target aliases, where supported, belong to target knowledge rather than arbitrary source-defined aliases.

Imports May Be Target-Selected

@target(os: "linux")
import "platform/linux"

When the selector does not match, the import does not enter the active source graph for that compilation plan.

This is declaration-aware source selection rather than an implicit runtime choice between modules.

File-local import semantics remain unchanged: an active import creates a qualifier only in the source file where it appears.

Configuration Selection with @when

@when selects the next top-level statement according to typed compile-time program configuration:

@when(config.telemetry)
fn SendTelemetry() Result[void, TelemetryError] {
    ...
}

The declaration either exists in the current compilation plan or it does not.

@when does not produce a runtime if.

Compile-Time Parameters

Compile-time parameters are typed program values supplied by project build configuration.

They are:

  • typed;
  • validated;
  • part of compilation-plan identity;
  • part of build-cache identity;
  • not compiler options;
  • not text macros;
  • not runtime configuration.

Boolean parameters are referenced in source through the canonical config.Name form:

@when(config.audit)
fn RecordAuditEntry(...) Result[void, AuditError] {
    ...
}

Compile-Time Parameters Come from Project Build Configuration

Project-wide values may be declared in the project manifest:

[parameters]
telemetry = false
maximum_connections = 1000
product_name = "Shop"

More specific build configuration may override them:

[target.shop.parameters]
maximum_connections = 2000
[variant.windows-amd64.parameters]
native_service = true
[target.shop.variant.windows-amd64.parameters]
service_name = "ShopService"

Command-line overrides may provide the final invocation-specific value when supported by the build interface.

More specific configuration overrides less specific configuration only under the project configuration rules.

Parameter Overrides Preserve Type

A parameter's type is established by its original declaration. Every override must preserve that type.

This is invalid:

[parameters]
maximum_connections = 1000

[target.shop.parameters]
maximum_connections = "many"

Conditional compilation therefore does not operate on untyped strings.

Compile-time parameters also must not contain secrets. Their values may become visible in generated code, IR, object files, debug information or build caches.

@when Condition Language

Sec 0.1 permits a deliberately restricted compile-time boolean expression:

boolean compile-time parameter
true
false
!
&&
||
==
!=
(...)

Examples:

@when(config.audit)
@when(!config.telemetry)
@when(config.audit && config.databaseLogging)

The expression must be evaluable while constructing the compilation plan. It does not invoke ordinary user functions or runtime computation.

@target and @when Have Different Responsibilities

Use @target for target identity:

@target(os: "linux")
fn InstallPlatformHook() void {
    ...
}

Use @when for typed program configuration:

@when(config.audit)
fn InstallAuditHook() void {
    ...
}

Combine the two when both restrictions apply:

@target(os: "linux")
@when(config.audit)
fn InstallAuditIntegration() void {
    ...
}

Do not mix target identity into the @when expression:

@when(config.audit && target.os == "linux")    // invalid in Sec 0.1

Keeping these domains separate makes target selection and program configuration explicit and independently validatable.

Compiler Options Are Not Conditional-Compilation Inputs

Compiler behavior settings do not decide whether declarations exist.

Examples that are not @when inputs include:

  • optimization level;
  • debug-information level;
  • warning level;
  • LTO;
  • strip configuration;
  • bounds-check lowering policy;
  • overflow-check lowering policy.

Changing optimization or debug configuration must not silently change the public source graph of the program.

Build features that genuinely change program source use typed compile-time parameters instead.

Combining @target and @when

@target(os: "linux", arch: "amd64")
@when(config.telemetry)
fn StartTelemetryBackend() Result[void, TelemetryError] {
    ...
}

The declaration is active only when both the target selectors and the @when condition are satisfied.

Consecutive selection attributes belong to the same declaration attachment set. They do not create nested scopes around one another.

Conditional Declaration Variants

Several declarations may represent one logical symbol when their selection conditions are mutually exclusive:

@target(device: "controller-a")
@address(0x40010000)
let mut GPIO: GPIORegisters

@target(device: "controller-b")
@address(0x50020000)
let mut GPIO: GPIORegisters

For one selected device, exactly one declaration becomes active.

The same mechanism can provide target-specific functions, imports, types, extern declarations, implementation blocks and other top-level source.

Variants Must Be Mutually Exclusive for One Plan

Reusing one logical declaration identity is valid only when no legal compilation plan can activate conflicting variants simultaneously.

Selection therefore becomes part of duplicate-declaration analysis.

Overlapping Active Variants Are an Error

@target(os: "linux")
fn Open() void {
}

@target(os: "linux", arch: "amd64")
fn Open() void {
}

On Linux amd64, both declarations are active. Compilation fails.

A diagnostic should identify all active declarations and the relevant compilation-plan facts.

There Is No Implicit “Most Specific Variant” Rule

Sec does not interpret:

@target(os: "linux", arch: "amd64")

as automatically preferred over:

@target(os: "linux")

If both define the same logical declaration and are active, the program is invalid.

Source selection is explicit, not precedence-based pattern matching.

Public Variant Shape Must Remain Compatible

Conditional source selection may change implementation details, but it must not silently turn one public API into unrelated APIs across targets.

Mutually exclusive variants of one public symbol therefore need compatible public shape.

Function Variant Shape

Public function variants must agree on:

  • name;
  • visibility;
  • generic parameter shape;
  • parameter types;
  • return type;
  • compatible declared effects;
  • compatible ABI contract.

Target-specific bodies may differ:

@target(os: "linux")
fn Open(path: string) Result[File, IOError] {
    ...
}

@target(os: "windows")
fn Open(path: string) Result[File, IOError] {
    ...
}

The public function contract remains stable even though platform implementation differs.

Variable Variant Shape

Public variable variants must agree on:

  • name;
  • visibility;
  • declared type;
  • mutability;
  • compatible ownership policy.

Their physical addresses and other target-private binding details may differ.

Type Variant Shape

Type variants follow the public identity and surface rules defined by the type and module model.

Target-specific private representation details may differ only where the owning layout/ABI rules permit that difference without changing the public semantic contract.

No Active Variant

If no declaration variant is active for the current compilation plan, that symbol is unavailable.

For example:

@target(device: "controller-a")
let mut GPIO: GPIORegisters

@target(device: "controller-b")
let mut GPIO: GPIORegisters

A build for controller-c has no active GPIO declaration.

A use should produce a target-aware diagnostic that reports the current plan and the available variants.

Module Identity Remains Stable Across Variants

Conditional compilation selects files and declarations inside the project/module model. It does not create a different module name for each target.

The same logical module may contain target-selected source, but its canonical module identity remains the module identity defined by the Modules chapter.

This is why statement-level @target does not attach to the module declaration itself.

Excluded Source Is Still Source

A file or declaration excluded by @target or @when:

  • is lexed;
  • is parsed;
  • is available to the formatter;
  • is visible to the LSP as excluded source;
  • does not enter the active symbol table;
  • does not require unavailable imports or types for the current plan;
  • does not undergo ordinary active semantic analysis;
  • does not reach Semantic IR;
  • does not reach lowering.

This design keeps inactive source structurally valid while allowing target-specific semantic dependencies to exist only on plans where they are meaningful.

Syntax Errors in Excluded Source Still Count

A selector cannot hide malformed Sec syntax indefinitely.

@target(os: "windows")
fn Broken( {    // syntax error even in a Linux plan
}

Because all source is parsed before active semantic selection, syntax errors remain syntax errors regardless of whether the declaration is active for the current plan.

Target-Dependent Semantic Errors Are Checked When Source Is Active

Excluded source does not undergo ordinary active Sema for the current plan.

This means an inactive declaration may refer to a target-only import or type unavailable in the current plan without forcing that dependency to exist.

When a plan activates the declaration, its semantic requirements become active and are checked normally.

Distinction: Syntax validity is universal across source. Active semantic validity is plan-specific.

LSP and Excluded Source

Editor tooling should preserve excluded source in the document model rather than pretending it does not exist.

The LSP should be able to show:

  • whether a file or declaration is active in the current workspace plan;
  • which @target selector excluded it;
  • which @when condition excluded it;
  • the active target/variant/profile context;
  • available mutually exclusive variants;
  • shape mismatches between public variants;
  • overlap under a selected plan;
  • completion for known target values and configuration names.

Formatting and syntax diagnostics remain available for excluded source.

Selection Participates in Compilation and Cache Identity

Compile-time parameters and target/variant identity can change the active source graph and generated artifact.

Build/cache identity therefore includes inputs such as:

  • project identity;
  • target identity;
  • variant identity;
  • operating system;
  • architecture;
  • ABI;
  • CPU and features;
  • selected profile;
  • compiler options;
  • compile-time parameters;
  • source hashes;
  • selected platform source.

Artifacts produced for one incompatible selection must never be reused as though they represented another plan.

Conditional Compilation Must Remain Reproducible

A compilation plan with the same semantic inputs should resolve the same active source graph.

Source selection therefore must not depend on:

  • filesystem enumeration order;
  • host machine details not represented in the selected target plan;
  • runtime environment variables unless explicitly imported into typed build configuration;
  • current wall-clock time;
  • network state;
  • arbitrary compiler implementation accidents.

Typed configuration and explicit target identity are part of the reason Sec avoids an unrestricted textual preprocessor.

Diagnostics

Unknown target selector

unknown @target selector `vendor`

Sec 0.1 selectors are:
    os
    arch
    cpu
    device
    board

Contradictory target selection

target selectors are incompatible

    arch = "arm32"
    cpu  = "x86-64"

the selected CPU does not belong to the selected architecture

Unknown configuration parameter

unknown compile-time parameter `config.telemetry`

help: declare the parameter in project build configuration
help: check the parameter spelling

Wrong parameter type

`@when` requires a boolean condition

`config.maximum_connections` has type int

Compiler option used as a source condition

compiler option `optimization` cannot control source inclusion

help: use a typed compile-time parameter when program source genuinely varies

Overlapping variants

multiple active target variants define `Open`

active declarations:
    platform.sec:10
    platform.sec:15

compilation plan:
    os = linux
    arch = amd64

help: make the selection conditions mutually exclusive

Public shape mismatch

target variants of `Open` have incompatible public shape

linux variant:
    fn Open(path: string) Result[File, IOError]

windows variant:
    fn Open(path: string, flags: uint32) Result[File, IOError]

one logical public symbol must keep a compatible public contract

No active variant

`GPIO` is unavailable for device "controller-c"

available variants:
    device = "controller-a"
    device = "controller-b"

Diagnostics should always include the relevant target/variant/configuration facts when those facts explain why a declaration is active, excluded or conflicting.

Formatter Behavior

The formatter preserves canonical attribute forms:

@target(
    os: "linux",
    arch: "arm64",
    cpu: "cortex-a72",
)

@when(config.audit && config.databaseLogging)
fn RecordAuditEntry(...) Result[void, AuditError] {
    ...
}

It must not:

  • move a file-level selector below the module declaration;
  • change file-level selection into declaration-level selection;
  • rewrite target identity as configuration state;
  • rewrite configuration state as target identity;
  • delete excluded source;
  • change variant overlap semantics.

Best Practices

  • Use file-level @target when an entire file belongs to one target family.
  • Use statement-level @target when only a small number of declarations differ.
  • Use @when for typed program features, not for compiler tuning.
  • Keep target identity in @target and program configuration in @when.
  • Prefer device or board for peripheral/address differences instead of over-broad CPU selection.
  • Make declaration variants obviously mutually exclusive.
  • Do not rely on a “more specific selector wins” rule; Sec has none.
  • Keep public API shape stable across target variants.
  • Keep inactive source syntactically valid and formatted.
  • Do not put secrets in compile-time parameters.
  • Use ordinary runtime if when the decision belongs to runtime state.
  • Avoid exploding one declaration into many variants when one portable implementation is sufficient.
  • Use target-selected imports only when the dependency itself is genuinely target-specific.

Summary

  • Sec conditional compilation is source selection for one concrete CompilationPlan.
  • The canonical selection attributes are @target and @when.
  • @target selects by target identity.
  • @when selects by typed compile-time program configuration.
  • Conditional compilation is not runtime branching.
  • Sec 0.1 has no C-style textual preprocessor.
  • Sec does not use #define, #ifdef, token substitution or conditional lexical regions.
  • All source is lexed and parsed before active semantic selection.
  • Selection occurs before active symbol-table construction and ordinary active Sema.
  • A file-level @target is the first source-bearing line and selects the complete file.
  • Comments and a supported byte-order marker may precede file-level @target.
  • A later @target selects only the next allowed top-level statement.
  • Statement-level @target may select imports and top-level declarations.
  • @target does not attach to the module declaration itself.
  • Module identity stays stable across variants.
  • Sec 0.1 target selectors are os, arch, cpu, device and board.
  • At least one target selector is required.
  • Omitted selectors impose no restriction.
  • All supplied target selectors combine with logical AND.
  • Unknown selector names are errors.
  • Target values are checked against canonical target knowledge and compatibility relationships.
  • os identifies an OS or freestanding environment family.
  • arch identifies an instruction-set architecture/family.
  • cpu identifies a processor core or implementation.
  • device identifies a complete MCU/SoC/target device.
  • board identifies a concrete hardware product containing a device and external components.
  • Device-specific peripheral differences should normally select by device or board rather than CPU alone.
  • An import may be selected with @target.
  • An excluded import does not enter the active source graph for the current plan.
  • @when selects the next top-level statement using a plan-time boolean condition.
  • Compile-time parameters are typed program values supplied by project build configuration.
  • Compile-time parameters are validated and participate in compilation/cache identity.
  • Compile-time parameters are not compiler options, textual macros or runtime configuration.
  • Boolean configuration is referenced through config.Name.
  • Project, target, variant and target-variant configuration may supply parameter values under the project rules.
  • Parameter overrides must preserve the declared/original parameter type.
  • Compile-time parameters must not contain secrets.
  • The Sec 0.1 @when language supports boolean parameters, true/false, !, &&, ||, ==, != and parentheses.
  • Target identity belongs in @target, not inside @when.
  • Compiler options such as optimization, debug info, warnings, LTO and strip settings do not control declaration existence.
  • Changing compiler tuning must not silently change the public source graph.
  • @target and @when may be combined on one declaration.
  • A combined declaration is active only when every attached selection condition is satisfied.
  • Mutually exclusive declaration variants may define one logical symbol.
  • Variant selection participates in duplicate-declaration analysis.
  • If two variants of one logical declaration are active in one plan, compilation fails.
  • Sec never implicitly selects the “most specific” active variant.
  • Mutually exclusive public variants must keep compatible public shape.
  • Function variants keep name, visibility, generic shape, parameter types, return type, effects and ABI contract compatible.
  • Variable variants keep name, visibility, type, mutability and ownership policy compatible.
  • Target-specific implementation bodies, addresses, vectors and private details may differ where their owning rules permit it.
  • If no variant is active, the symbol is unavailable for that compilation plan.
  • Unavailable-symbol diagnostics should report relevant target facts and available variants.
  • Excluded source is still lexed, parsed, formatted and visible to the LSP.
  • Excluded source does not enter the active symbol table, active Sema, Semantic IR or lowering.
  • Excluded source does not require target-only imports/types to exist in the current plan.
  • Syntax errors remain errors even in excluded source.
  • Target-dependent semantic errors are evaluated when their source becomes active.
  • LSP tooling should expose active/excluded state and the condition responsible for it.
  • Target/variant/configuration inputs participate in build and cache identity when they can affect generated output.
  • Artifacts from incompatible compilation plans must not be reused interchangeably.
  • Conditional selection must be reproducible from explicit plan inputs rather than ambient runtime state.