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 Model
An initialized union value contains:
- one active declared variant;
- 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
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
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.
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.
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.
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:
- both values have the same union type;
- 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.
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.
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;
emptyused where the type or flow state cannot be empty;- impossible
isstate test; - unknown variant in
isormatch; - duplicate or unreachable match coverage;
- non-exhaustive union match, including reachable
emptystate; - invalid destructuring field name;
- duplicate destructured field;
- illegal
reforref mutpattern 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
defaultvariant. - A mutable non-defaultable union binding may begin in the compiler-known
emptyinitialization state. emptyis 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 Varianttests the active variant without payload binding.is emptyobserves definite-initialization state.istests refine compiler flow state where possible.matchis the primary exhaustive union inspection and payload-binding construct.- Reachable
emptystate 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
reforref 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
whereand do not by themselves exhaust a variant. Option.NoneandResult.Errare actual values and are not unionempty.type Name union errordeclares a payload-bearing concrete Sec error type.- Concrete union errors widen to
errorwhile 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" unionis a separate overlapping foreign representation, not an ordinary tagged Sec union.