Unions

A Sec union is a closed nominal type whose initialized value contains exactly one active declared variant. Variants may carry no data, one unnamed payload, or a struct-like payload with named fields.

type Message union {
    Quit
    Text(string)

    Move {
        x: int
        y: int
    }
}

Unions are used when one value may be one of several explicitly declared alternatives and variant-specific data must remain statically typed.

Core rule: A union is closed over its declared variants. The active variant is semantic state, not a source-visible integer tag.

Core Model

An initialized union value contains:

  1. one active declared variant;
  2. the payload required by that variant, if any.

A union is distinct from several related concepts:

Construct Difference from a union
Enum Represents a named value from a value domain; enum members do not carry variant payload data
Struct All declared fields exist together
Interface Describes behavior rather than one-of-many stored variants
Option[T] None is an actual value of the option type
empty Compiler-known initialization state, not a union value or variant
null Not Sec's union model

Every union has these properties:

  • it is named;
  • it is nominal;
  • it contains at least one variant;
  • variant names are unique within the union;
  • different unions may reuse variant names;
  • duplicate payload types are allowed;
  • variants cannot be added outside the declaration;
  • variants are constructors, not independent runtime types;
  • distinct union types are never implicitly interchangeable.

Declaration

The declaration form is:

type Name union {
    variants
}

Example:

type State union {
    Idle
    Running
    Stopped
}

A union must be named and must contain at least one variant.

Anonymous union types are not part of the Sec union model.

Variant Kinds

One union may freely mix all three variant forms.

Payload-less variant

type State union {
    Idle
    Running
    Stopped
}

A payload-less variant represents only its variant identity.

Single unnamed payload

type Number union {
    Integer(int)
    Decimal(decimal)
}

A single-payload variant carries exactly one value.

Multiple unnamed payload parameters are not supported:

type Invalid union {
    Pair(int, int)    // invalid
}

Use a struct-like payload when several values belong to one variant.

Struct-like payload

type Shape union {
    Circle {
        radius: decimal
    }

    Rectangle {
        width: decimal
        height: decimal
    }
}

The struct-like payload belongs to the variant. It is not a separate nominal source-level type.

Mixed union

type Message union {
    Quit
    Text(string)

    Move {
        x: int
        y: int
    }
}

Variant Namespace

Union variants belong to the union namespace.

type State union {
    Idle
    Running
}

Construction uses qualified names:

let state := State.Idle

Declaring State.Idle does not inject an ordinary module-level symbol named Idle.

Pattern contexts may resolve an unqualified variant name from the known subject union type:

match state {
    Idle => HandleIdle()
    Running => HandleRunning()
}

Qualification remains available when required for clarity or disambiguation.

Construction

Payload-less variant

let state := State.Idle

A payload-less variant accepts no arguments.

let state := State.Idle(1)    // invalid

Single-payload variant

let number := Number.Integer(42)

Exactly one value assignable to the declared payload type is required.

Number.Integer()       // invalid
Number.Integer(1, 2)   // invalid

Struct-like variant

let shape := Shape.Rectangle {
    width: 10
    height: 20
}

Struct-like payload construction uses named fields.

Positional construction is invalid:

Shape.Rectangle(10, 20)    // invalid
Rule: The selected variant is always explicit in union construction. There is no implicit payload-to-union conversion.

Omitted Struct-Like Payload Fields

A struct-like payload field may be omitted when its declared field type is Defaultable.

type MoveCommand union {
    Move {
        x: int
        y: int
    }
}

let command := MoveCommand.Move {
    x: 10
}

The constructed payload is semantically equivalent to:

MoveCommand.Move {
    x: 10
    y: 0
}

A field whose type is NonDefaultable must be supplied explicitly.

The compiler also rejects:

  • unknown construction fields;
  • duplicate fields;
  • values incompatible with the declared field type.

These are real semantic defaults. Omission never creates undefined payload storage.

Payload Ownership During Construction

Constructing a union follows ordinary Sec copy and move rules. Union construction never silently consumes a reusable source Place.

Copyable payload

let value: int := 42
let number := Number.Integer(value)

A copyable source may be copied according to normal value semantics.

Move-only payload

type Choice[T] union {
    Some(T)
    None
}

let resource := OpenResource()
let choice := Choice[Resource].Some(<-resource)

When a reusable source is non-copyable, ownership transfer is explicit.

The same rule applies to struct-like payload fields:

type Work union {
    Pending {
        Resource: Resource
    }

    Done
}

let resource := OpenResource()

let work := Work.Pending {
    Resource: <-resource
}

Fresh temporaries need no redundant move marker:

let choice := Choice[Resource].Some(OpenResource())

Union Defaults

A union does not automatically use its first declared variant as a default.

This differs intentionally from enum default semantics.

type State union {
    Idle
    Running
}

State is not defaultable merely because Idle appears first.

Explicit default variant

At most one variant may be marked default.

Payload-less default

type State union {
    Idle default
    Running
}

Single-payload default

type Value union {
    Number(int) default
    Text(string)
}

The payload type must itself be defaultable.

Struct-like default

type Position union {
    Unknown

    Known {
        x: int
        y: int
    } default
}

Every required payload field must be resolvable through the ordinary struct-like payload default rules.

Defaultability

A union is Defaultable exactly when it declares one valid explicit default variant.

type State union {
    Idle default
    Running
}

let mut state: State

is semantically equivalent to:

let mut state: State := State.Idle

Immutable declarations still require an initializer:

let state: State    // invalid
Rule: Union defaultability is explicit intent. Declaration order alone never chooses a union default.

The empty Initialization State

A mutable binding of a NonDefaultable union type may omit its initializer as a union-specific definite-assignment form.

type State union {
    Idle
    Running
}

let mut state: State

Immediately after the declaration, state is empty.

This means that storage exists for the binding, but no union value has yet been constructed there.

Important: empty is compiler-known initialization state. It is not a hidden union variant and is not part of the union's closed value domain.

empty is not:

  • null;
  • None;
  • Nothing;
  • a source-visible numeric tag;
  • a value that can be passed as an ordinary union value.

Restrictions on Empty Bindings

An empty union binding does not contain a union value and therefore cannot be used where an actual union value is required.

Before initialization, it may not be:

  • read as a value;
  • passed to a function as a union value;
  • returned;
  • copied;
  • moved;
  • borrowed as a union value;
  • stored into another value.

Assigning a valid variant activates the union:

let mut state: State
state = State.Running

UseState(state)

After the assignment, state contains an initialized State.Running value.

Destruction of still-empty storage

If local union storage remains empty until its storage lifetime ends, no union payload is destroyed because no payload was ever constructed.

Runtime-dependent empty state

let mut state: State

if condition {
    state = State.Idle
}

Later code may need to distinguish the initialized and empty paths. The compiler may internally carry an initialization flag or equivalent state when runtime control flow requires it.

That implementation state never becomes a source-visible union variant.

Moved-From Is Not Empty

A moved-from union binding follows ordinary ownership rules. It does not become a union-specific empty binding.

let mut source := State.Idle
let target :<- source

if source is empty {
    ...
}

This is invalid. source is moved-from and unavailable, not empty.

The distinction matters because empty is an initialization state defined only for applicable union storage. A move is an ownership event.

State Tests with is

Active variant test

is may test the active variant without destructuring or binding its payload:

if state is Idle {
    HandleIdle()
}

The same form applies to a payload-carrying variant:

if result is Ok {
    HandleSuccessState()
}

No payload binding is introduced by is Variant. Use match when payload extraction is required.

Empty-state test

if state is empty {
    state = State.Idle
}

is empty observes definite-initialization state rather than a union variant.

Data-flow refinement

let mut state: State

if state is empty {
    state = State.Idle
}

UseState(state)

After the if, the compiler can prove state initialized:

  • the true path initializes it;
  • the false path proves it was already non-empty.

Similarly, within:

if state is Running {
    ...
}

the compiler knows the subject is initialized and has active variant Running.

An is empty test is meaningless in a context that guarantees an actual initialized value, such as an ordinary by-value function parameter.

Matching Unions

match is the primary operation for exhaustive union inspection and payload binding.

Payload-less variants

match state {
    Idle => HandleIdle()
    Running => HandleRunning()
    Stopped => HandleStopped()
}

Single-payload variants

match number {
    Integer(value) => UseInt(value)
    Decimal(value) => UseDecimal(value)
}

Possible empty state

If control-flow analysis says a union binding may still be empty, exhaustive matching must include that state:

match state {
    empty => Initialize()
    Idle => Wait()
    Running => Work()
}

When the subject is known to contain an actual union value, empty is not part of exhaustiveness.

An unguarded catch-all _ covers every remaining reachable state, including empty when it is reachable.

Tooling may warn when a general catch-all hides a reachable empty state, because accidental handling of uninitialized storage can obscure a logic error.

Whole-Payload Binding

A complete single payload may be bound in the normal variant pattern:

match number {
    Integer(value) => UseInt(value)
    Decimal(value) => UseDecimal(value)
}

A complete struct-like payload may also be bound as one value:

match shape {
    Circle(circle) => circle.radius
    Rectangle(rectangle) => rectangle.width * rectangle.height
}

This form is useful when the payload should be passed around as one coherent value inside the selected arm.

Shallow Named-Field Destructuring

A struct-like variant may bind named payload fields directly:

match shape {
    Circle { radius } => radius * radius
    Rectangle { width, height } => width * height
}

Field destructuring is shallow. A field that itself contains structured or variant data is handled through ordinary field access or another match.

Partial binding

match shape {
    Rectangle { width } => Use(width)
    _ => Other()
}

Omitted fields are not bound. Their omission does not add a value restriction and does not apply defaults.

No .. marker is needed merely to ignore unbound fields.

Binding rename

match shape {
    Rectangle {
        width: w
        height: h
    } => w * h

    _ => 0
}

Rectangle { width } is shorthand for binding the field width locally as width.

No recursive nested patterns

Sec 0.1 union destructuring does not create a recursive nested pattern language.

Some(Ok(value)) => Process(value)

is not the canonical form. Match the nested value separately.

Borrowed Pattern Fields

Struct-like payload destructuring follows ordinary borrowing rules.

Shared borrow

match shape {
    Rectangle {
        width: ref w
        height: ref h
    } => Inspect(w, h)

    _ => Other()
}

Mutable borrow

match shape {
    Rectangle {
        width: ref mut w
    } => Modify(w)

    _ => Other()
}

Mutable borrowing requires that the match subject and surrounding borrow state provide the necessary authority.

Pattern borrows are scoped to the applicable arm and follow the ordinary lifetime and overlap rules.

No Hidden Partial Moves

Field destructuring must never silently leave a reusable union value in a partially moved state.

A plain by-value field binding is valid only when the field type is implicitly copyable:

Packet { count } => Use(count)

If a field is move-only, borrow it:

Packet { payload: ref data } => Inspect(data)

or:

Packet { payload: ref mut data } => Modify(data)

When ownership of move-only variant content is required, bind the complete variant payload by value and perform ownership operations on that complete payload.

Rule: Union patterns never silently clone payload data, silently borrow instead of moving, or introduce hidden partial ownership state.

Match Guards

Union patterns use the normal match guard syntax:

match message {
    Move { x, y } where x == y => HandleDiagonal(x)
    Move { x, y } => HandleMove(x, y)
    _ => Other()
}

Bindings introduced by the pattern are visible in the guard.

A guarded arm does not by itself exhaust the underlying union variant because the guard may evaluate to false.

Guard evaluation and ownership commit follow the general match ownership rules.

Result and Option

Result[T, E] and Option[T] use the same union-style matching model where their own compiler-known rules define it.

match result {
    Ok(value) => Use(value)
    Err(error) => Handle(error)
}
match option {
    Some(value) => Use(value)
    None => Missing()
}

None is an actual Option[T] value. Err(error) is an actual Result[T, E] variant.

Neither is the union initialization state empty.

Rule: Do not use union empty as a substitute for optionality or failure. Use Option, Result and the error model for those semantics.

Error Unions

A tagged union may be declared as a concrete Sec error type when variants need payload data.

type DetailedError union error {
    OpenError {
        Path: string
        Code: int
    }

    ReadError {
        Path: string
        Offset: uint64
        Code: int
    }
}

The final error marker declares the complete union as a concrete error type.

The union remains an ordinary closed nominal tagged union. The error marker does not alter:

  • its variant set;
  • payload types;
  • layout semantics;
  • ownership;
  • borrowing;
  • destruction;
  • defaultability;
  • match behavior.

Its additional property is that the complete concrete error value may widen implicitly to the compiler-known root error.

fn Read() Result[Data, DetailedError] {
    ...
}

fn Run() Result[Data, error] {
    let data := try Read()
    return Ok(data)
}

Widening preserves concrete error identity, active variant, payload, ownership state and destruction behavior.

The root error domain is open even though DetailedError itself remains closed.

Error payloads may own ordinary Sec values, including move-only resources. There is no hidden clone merely because a union is an error type.

Assignment and Conversion

A union value is assignable only where its exact union type is compatible.

let value: Number := Number.Integer(42)

A payload does not implicitly become a union:

let value: Number := 42    // invalid

A union does not implicitly become its active payload:

let value: int := Number.Integer(42)    // invalid

Distinct union types remain different even when they contain identical-looking variants and payload types.

type Left union {
    Value(int)
}

type Right union {
    Value(int)
}

let left := Left.Value(10)
let right: Right := left    // invalid

The union rule does not define direct casts between a union and one of its payload types, nor between unrelated union types.

Equality

Two union values may use ordinary equality when:

  1. both values have the same union type;
  2. all payload possibilities required for equality are equality-comparable.

Payload-less unions are equality-comparable.

Variant identity is part of equality:

type Value union {
    First(int)
    Second(int)
}

let first := Value.First(1)
let second := Value.Second(1)

if first != second {
    ...
}

The values are unequal even though both payloads contain integer 1.

empty does not participate in union equality because it is not a union value.

Copy and Move Behavior

A union is copyable only when all possible variant payloads satisfy the ordinary copy rules.

A payload-less union is copyable:

type State union {
    Idle
    Running
}

A union containing any non-copyable possible payload is not generally copyable as a complete value.

type ResourceState union {
    Empty
    Owned(Resource)
}

If Resource is move-only, ResourceState must preserve that ownership restriction.

Assignment, parameter passing, returns and pattern binding use the ordinary Sec copy/move/borrow model. The union construct does not add hidden copying.

Destruction runs only for the currently active payload and follows that payload's ordinary destruction semantics.

Generic Unions

A union may use ordinary generic parameter syntax:

type Option[T] union {
    Some(T)
    None
}
type Result[T, E] union {
    Ok(T)
    Err(E)
}

Generic parameters may appear in:

  • single payload types;
  • struct-like payload fields;
  • nested generic types;
  • references;
  • arrays and sequences;
  • other valid type forms.

Every complete generic instantiation is a distinct concrete nominal union type.

Choice[int]
Choice[string]

These remain different types even if an optimization could use the same physical representation.

Nested Unions

A union may be declared as an associated nested type where nested type declarations are otherwise permitted.

type Token struct {
}

impl Token {
    type Kind union {
        Identifier
        Number
        Symbol
    }
}

Outside the owner implementation, use the qualified name:

Token.Kind.Identifier

Nesting changes naming and lookup. It does not change the underlying union semantics.

Recursive Unions

Direct recursive storage is invalid because it would require infinite size.

type List[T] union {
    Node {
        value: T
        next: List[T]
    }

    End
}

This is invalid because List[T] would contain itself directly.

Recursion is valid through an indirection form whose own storage and ownership rules make the complete representation finite.

type NodeList[T] union {
    Node {
        value: T
        next: Box[NodeList[T]]
    }

    End
}

The exact legality and semantics of the indirection are defined by the corresponding type, allocation, ownership and storage rules.

Rule: Recursive type graphs are permitted when the stored representation remains finite. Direct recursive storage is not.

Behavior with Impl

A union is a nominal Sec type and may participate in ordinary impl declarations.

type Number union {
    Integer(int)
    Decimal(decimal)
}

impl Number {
    fn IsInteger() bool {
        return self is Integer
    }
}

Methods, properties, associated declarations and interface conformance follow the normal impl rules.

A variant is not a separate nominal type and cannot receive its own unrelated implementation block:

impl Number.Integer {
    ...
}

This is invalid. Integer is a constructor and pattern alternative belonging to Number.

An impl cannot add variants to the closed union declaration.

Memory Representation

Conceptually, an initialized union value requires:

  • an active-variant discriminator;
  • storage large and aligned enough for the active payload.

Every variant has a stable compiler-internal identity used for semantic analysis and lowering.

Those identities are not source-level integer tags.

The compiler may optimize physical representation when all observable union semantics are preserved.

Representation of empty state

empty is not a declared discriminator value.

When runtime flow requires empty-state observation, the compiler may represent initialization through:

  • SSA state;
  • an initialization bit;
  • control-flow facts;
  • another internal mechanism.

That mechanism must not:

  • become a source-visible variant;
  • change union equality;
  • change the declared variant set;
  • escape through ordinary union value passing.

Foreign C Unions

An extern "C" union is not an ordinary tagged Sec union.

extern "C" type NativeValue union {
    ...
}

A foreign C union follows the active C ABI representation:

  • its members physically overlap;
  • it has no hidden Sec variant discriminator;
  • the compiler does not track one active Sec variant in the ordinary union sense;
  • direct member access is classified as foreign-unsafe.

Ordinary Sec unions remain tagged, closed and governed by the semantics described throughout this chapter.

Do not conflate them: a Sec union and a C union are different language constructs even if both use the word union.

Visibility

Union type visibility follows the ordinary named-type visibility rules.

Variant visibility is governed by the variant declaration and its owning union.

A public union may not expose payload types that violate ordinary public API visibility rules.

type PublicMessage union {
    Data(PrivateType)    // invalid if PrivateType cannot appear in this public API
}

Diagnostics

Important union diagnostics include:

  • anonymous union declaration;
  • union with no variants;
  • duplicate variant name;
  • unknown payload type;
  • duplicate struct-like payload field;
  • invalid direct recursive storage;
  • wrong single-payload construction arity;
  • invalid payload value type;
  • missing non-defaultable struct-like payload field;
  • unknown or duplicate construction field;
  • multiple default variants;
  • default variant that cannot be default-constructed;
  • read, pass, return, move or borrow of an empty binding as a union value;
  • empty used where the type or flow state cannot be empty;
  • impossible is state test;
  • unknown variant in is or match;
  • duplicate or unreachable match coverage;
  • non-exhaustive union match, including reachable empty state;
  • invalid destructuring field name;
  • duplicate destructured field;
  • illegal ref or ref mut pattern binding;
  • hidden or illegal partial move;
  • visibility violation.

Ownership diagnostics should identify the union Place, the active or possible variant involved, the transfer or borrow operation, and the later invalid use whenever those locations are available.

Summary

  • A Sec union is a closed nominal tagged type.
  • An initialized union value has exactly one active declared variant.
  • A variant may be payload-less, carry one unnamed payload, or carry a struct-like named payload.
  • Different variant kinds may be mixed within one union.
  • Variants belong to the union namespace and are constructors, not independent nominal types.
  • Struct-like variants use named-field construction.
  • Omitted struct-like payload fields receive semantic defaults only when their field types are defaultable.
  • Union construction obeys normal copy and move rules.
  • A reusable move-only payload requires explicit <- transfer.
  • A union does not acquire a default merely because one variant is declared first.
  • A union is defaultable exactly when it declares one valid explicit default variant.
  • A mutable non-defaultable union binding may begin in the compiler-known empty initialization state.
  • empty is not a union variant or union value.
  • An empty binding cannot escape or be used as an initialized union value.
  • Assigning a valid variant transitions empty storage to an initialized union value.
  • A moved-from union is unavailable; it does not become empty.
  • is Variant tests the active variant without payload binding.
  • is empty observes definite-initialization state.
  • is tests refine compiler flow state where possible.
  • match is the primary exhaustive union inspection and payload-binding construct.
  • Reachable empty state participates in match exhaustiveness.
  • Complete struct-like payloads may be bound as one value.
  • Struct-like payloads support shallow named-field destructuring, partial binding and rename.
  • Pattern fields may be borrowed with ref or ref mut.
  • Shallow destructuring never introduces hidden move-only partial moves.
  • Nested recursive pattern syntax is not part of the current Sec 0.1 union pattern model.
  • Match guards use where and do not by themselves exhaust a variant.
  • Option.None and Result.Err are actual values and are not union empty.
  • type Name union error declares a payload-bearing concrete Sec error type.
  • Concrete union errors widen to error while preserving concrete identity and payload.
  • There is no implicit payload-to-union or union-to-payload conversion.
  • Distinct union types remain nominally distinct.
  • Union equality requires the same union type and equality-comparable payload possibilities.
  • Variant identity participates in equality.
  • A union is copyable only when every possible payload satisfies the copy rules.
  • Generic unions are supported and each concrete instantiation is a distinct nominal type.
  • Nested unions follow ordinary nested-type naming and lookup rules.
  • Direct recursive union storage is invalid; finite recursion through valid indirection is permitted.
  • A union may have ordinary behavior through impl.
  • The internal active-variant discriminator is not exposed as a source-level integer.
  • extern "C" union is a separate overlapping foreign representation, not an ordinary tagged Sec union.