Enums

An enum is a nominal value type whose members give names to a defined set of underlying values. Sec enums may use integer or string representations, may participate in the typed error system, may be generic, and may also describe exact-width hardware encodings.

enum Direction {
    North
    East
    South
    West
}

Enum members are values of the enum type itself. They are not untyped constants and do not implicitly become their underlying integer or string representation.

Core distinction: Ordinary enums are closed over their declared value classes. bit-backed hardware enums are open over every bit pattern representable by their declared width.

Core Model

Sec has two semantic enum domains:

Enum kind Domain Typical purpose
Ordinary enum Closed over declared underlying value classes Application states, identifiers, protocols, errors
Bit-backed enum Open over every value representable by bit[N] Hardware register fields and raw device encodings

This distinction is semantic. It does not depend merely on the machine representation chosen by the backend.

An ordinary enum backed by uint8 is still closed. A bit[8] enum is open even if both eventually occupy eight bits.

Declaration Forms

The ordinary form is:

enum Color {
    Red
    Green
    Blue
}

The equivalent explicit type-declaration form is also valid:

type Color enum {
    Red
    Green
    Blue
}

An enum must contain at least one member.

Members may be separated by line breaks or commas where the grammar permits it, and a trailing comma is allowed:

enum Direction {
    North,
    East,
    South,
    West,
}

Underlying Types

An ordinary enum without an explicit underlying type uses int.

enum Status {
    Unknown
    Active
    Disabled
}

is an ordinary int-backed enum.

Explicit integer representation

enum HttpStatus uint16 {
    Ok = 200
    NotFound = 404
}

An ordinary enum may use any valid integer type as its underlying representation. The selected type is part of the enum declaration and constrains the values representable by its member initializers.

enum SmallState uint8 {
    Off = 0
    On = 1
}

enum LargeCode uint128 {
    First = 1
    Second = 2
}

String representation

enum Program string {
    OneCare = "Zebra OneCare"
    VIQ = "Z1C+VIQ"
    AdvancedExchange = "Advanced Exchange"
}

A string-backed enum remains a nominal enum. Its members are values of Program, not ordinary string values.

Hardware bit representation

enum DeviceMode bit[2] {
    Off = 0b00
    Active = 0b01
    Sleep = 0b10
}

bit is shorthand for bit[1] in enum underlying position. Bit-backed enum widths are language-defined from 1 through 256 bits.

Members and Namespace

Enum members are named typed constants in the enum's namespace.

enum Direction {
    North
    East
    South
    West
}

let direction: Direction := Direction.North

Member names must be unique within one enum. Different enum types may reuse the same member names.

enum Direction {
    North
    South
}

enum Pole {
    North
    South
}

let direction := Direction.North
let pole := Pole.North

An enum member has the nominal enum type, not its underlying representation:

let value: int := Direction.North    // invalid implicit conversion

Convert explicitly when the underlying value is required.

Integer Member Initializers

An explicit enum member initializer uses =:

enum HttpStatus int {
    Ok = 200
    NotFound = 404
}

The initializer must be a compile-time integer constant expression representable by the enum's underlying integer type.

This is not canonical initializer syntax:

enum Status int {
    Ready: 1
}

Use:

enum Status int {
    Ready = 1
}

Integer-backed enums have an additional compile-time mechanism, iota, for concise value sequences.

iota

iota is a compile-time integer constant available only while evaluating integer-backed enum member initializers.

Its value is the zero-based declaration position of the current member.

enum Direction {
    North
    East
    South
    West
}

is equivalent to beginning the sequence with:

enum Direction {
    North = iota
    East
    South
    West
}

and resolves to:

North = 0
East  = 1
South = 2
West  = 3

Explicit numeric values do not reset iota. Each enum has its own independent iota sequence.

iota is not a general language constant and is not available in string-backed enums.

Initializer Repetition

After the first integer-backed member, omitting an initializer repeats the preceding initializer expression and evaluates that expression using the current member's iota.

enum Code int {
    A = iota    // 0
    B           // 1
    C           // 2
    D = 10      // 10
    E           // 10
}

E becomes 10 because the expression 10 is repeated. Sec does not interpret omission as "previous resolved value plus one".

To jump and continue a sequence from the new position, express the jump in terms of iota:

enum Code int {
    A = iota        // 0
    B               // 1
    C               // 2
    D = iota + 7    // 10
    E               // 11
    F               // 12
}

Bit-mask style sequences

enum Permission uint {
    None = 0
    Read = 1 << iota
    Write
    Execute
}

resolves to:

None    = 0
Read    = 2
Write   = 4
Execute = 8

Every repeated expression is checked again for overflow and representability after substituting the current iota.

String-Backed Enums

A string-backed enum uses string as its explicit underlying type.

enum Program string {
    OneCare = "Zebra OneCare"
    VIQ = "Z1C+VIQ"
    AdvancedExchange = "Advanced Exchange"
}

Every member must have an explicit compile-time string initializer.

This is invalid:

enum Program string {
    OneCare = "Zebra OneCare"
    VIQ
}

String enums have no implicit sequence and no initializer-repetition rule. iota is not available.

String-backed enums are closed ordinary enums. Only the declared underlying string value classes are valid enum values.

let program := Program("Zebra OneCare")

This constant conversion is valid because the string names a declared value class.

let program := Program("Unknown")

This is a compile-time error when no member declares "Unknown".

Runtime conversion from string is checked:

let program := try Program(input)

Failure reports EnumValueError.UndeclaredValue.

Conversion back to the underlying string is explicit and infallible:

let text := string(Program.OneCare)

Value Aliases

More than one enum member name may have the same underlying value.

enum ResultCode int {
    Success = 0
    Ok = 0
    NotFound = 404
    Missing = 404
}

Duplicate underlying values are valid aliases. The member names remain distinct declared names, but values with the same underlying representation belong to one runtime value class.

The same applies to string-backed enums:

enum Program string {
    OneCare = "Zebra OneCare"
    LegacyOneCare = "Zebra OneCare"
}

Aliases compare equal:

if ResultCode.Success == ResultCode.Ok {
    ...
}

They also represent one coverage class for exhaustive match. Matching one alias covers that underlying runtime value.

Enum Defaults

Every valid non-empty enum is defaultable.

Implicit default

Unless another member is explicitly marked default, the first declared member is the enum's semantic default.

enum Status int {
    Unknown = 10
    Active = 20
}

The default is:

Status.Unknown

Numeric zero has no special priority.

Explicit default member

enum ConnectionState {
    Connecting
    Connected
    Disconnected default
}

Exactly one member may be marked default.

The marker appears after the member name and before an optional initializer:

enum ConnectionState int {
    Connecting = 10
    Connected = 20
    Disconnected default = 30
}

The default marker changes default selection only. It does not alter the initializer expression, iota, repetition rules or alias semantics.

Default initialization

let mut state: ConnectionState

initializes state to the enum's semantic default. This is not uninitialized storage.

The ordinary immutable-binding rule still requires an initializer:

let state: ConnectionState    // invalid

Ordinary Enums Are Closed

An ordinary enum's semantic domain contains only the unique underlying values declared by its members.

enum Color int {
    Red = 1
    Green = 2
    Blue = 3
}

Valid Color values are the declared value classes:

1
2
3

The fact that int can represent many other values does not make those values valid Color values.

The same closed-domain rule applies to string-backed enums.

Rule: Safe Sec semantics cannot produce an ordinary enum value outside its declared value domain.

Error Enums

A closed enum may be declared as a concrete Sec error type by adding the error marker.

enum IOError error {
    OpenError
    ReadError
    WriteError
}

When an explicit enum representation is needed, the error marker follows the ordinary representation information:

enum ProtocolError uint16 error {
    InvalidFrame = 1
    UnsupportedVersion = 2
}

The enum remains a closed nominal enum. Its additional semantic property is that values of the enum are assignable to the compiler-known root error type.

fn Read() Result[Data, IOError] {
    return Err(IOError.ReadError)
}

A concrete enum error may widen to error:

fn ReadOpen() Result[Data, error] {
    return Err(IOError.ReadError)
}

Widening preserves the concrete error type and member identity.

The reverse conversion is not implicit:

IOError -> error      // valid widening
error -> IOError      // not implicit
IOError -> ParseError // not implicit

Declaring an enum as an error type does not introduce general inheritance, exceptions or a mandatory error-object layout.

Error variants that need payload data belong to a union error rather than an enum error:

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

    ReadError {
        Path: string
        Offset: uint64
    }
}

Generic Enums

Enums may declare ordinary Sec generic type parameters.

enum SortOrder[T] {
    Ascending
    Descending
}

Every complete concrete argument list produces a distinct nominal enum type:

SortOrder[User]
SortOrder[Product]

These types remain distinct even when the generic parameter contributes no runtime storage and the physical representation is identical.

Once concretized, ordinary enum rules apply to members, defaults, conversions, equality, switch, match and representation.

Generic enum parameters may participate in type identity and associated behavior without becoming runtime generic parameters.

Rule: Generic enum instantiation does not introduce runtime type erasure, hidden dictionaries, generic dispatch or implicit boxing.

Enum members remain payload-less. If each alternative needs to carry different data, use a union.

Nested Enums

An enum may be declared as an associated nested type inside an owning impl.

impl Vehicle {
    enum FuelType {
        Petrol
        Diesel
        Electric
    }
}

Outside the implementation, use the qualified type and member names:

Vehicle.FuelType
Vehicle.FuelType.Diesel

Inside impl Vehicle, the shorter nested name may be used when normal scope resolution makes it unambiguous:

FuelType
FuelType.Diesel

Behavior and Associated Values

An enum is a nominal type and may have ordinary Sec behavior through impl.

enum Status {
    Ready
    Waiting
    Failed
}

impl Status {
    fn IsTerminal() bool {
        return self == Status.Failed
    }

    let DefaultFailure: Status := Status.Failed
}

Instance methods receive implicit self. Immutable associated values declared directly with let are type-associated members:

let failure := Status.DefaultFailure

Associated values are not enum members. They do not alter the enum's underlying value domain, member initialization, aliases, iota, default-member selection or exhaustive match coverage.

An implementation may also provide static methods, properties and other implementation members permitted by the general impl rules.

impl cannot add new enum members to an already declared enum.

Bit-Backed Hardware Enums

A hardware enum may use bit or bit[N] as its underlying representation.

enum AlertPinMode bit[2] {
    AlertDisabled = 0b00
    Comparator = 0b01
    Interrupt = 0b10
}

Unlike ordinary enums, a bit-backed enum is open over the complete domain representable by its bit width.

For bit[2], all four bit patterns are valid:

0b00
0b01
0b10
0b11

The first three have declared names in the example. 0b11 is still a valid AlertPinMode value even though it is unnamed.

This is necessary because hardware can expose reserved, undocumented, device-specific or future encodings. Sec preserves those values rather than trapping or inventing a member name.

Hardware rule: A bit[N] enum does not need to enumerate all 2^N possible values.

Unknown values in match

match mode {
    AlertPinMode.AlertDisabled => HandleDisabled()
    AlertPinMode.Comparator => HandleComparator()
    AlertPinMode.Interrupt => HandleInterrupt()
    _ => HandleUnknownMode()
}

When undeclared bit patterns remain possible, listing only named members is not exhaustive.

Bit Enums in Register Fields

A named register field may use a bit-backed enum when the enum width exactly matches the field width.

enum AlertPinMode bit[2] {
    AlertDisabled = 0b00
    Comparator = 0b01
    Interrupt = 0b10
}

type AlertControl register[8] {
    Mode: AlertPinMode
    _: bit[6]
}

Reading Mode preserves the complete raw two-bit pattern. If the hardware returns 0b11, the read still produces a valid AlertPinMode value.

Ordinary closed enums are not interchangeable with bit-backed register-field types merely because their numeric representation could fit.

Conversions

Enums do not implicitly convert to or from their underlying representation. Different enum types do not implicitly convert to each other.

Enum to integer

let raw := int(Color.Red)

Normal checked integer conversion rules apply to the target integer type.

Enum to string

let raw := string(Program.OneCare)

For a valid string-backed enum, conversion to string is infallible.

Constant integer to closed enum

enum Color int {
    Red = 1
    Green = 2
    Blue = 3
}

let color := Color(2)

This is valid because 2 is a declared Color value class.

let color := Color(9)

This is a compile-time error because 9 is not a declared value of the closed enum.

Runtime integer to closed enum

let color := try Color(raw)

When the compiler cannot prove that the runtime integer names a valid enum value, the conversion is fallible.

Possible failures are:

EnumValueError.UndeclaredValue
EnumValueError.OutOfRange

OutOfRange means the value cannot be represented by the enum's underlying representation. UndeclaredValue means the value fits the representation but does not belong to the closed enum domain.

Integer to open bit enum

enum DeviceMode bit[2] {
    Off = 0b00
    Active = 0b01
    Sleep = 0b10
}

let mode := DeviceMode(0b11)

This is valid because 0b11 fits the two-bit domain. It is a valid unnamed value.

A runtime conversion requires try only when width fit cannot be proven:

let mode := try DeviceMode(raw)

An in-range unnamed bit pattern never fails with EnumValueError.UndeclaredValue.

String to string-backed enum

let program := try Program(input)

Runtime conversion checks whether the input equals one of the declared string value classes. Failure is EnumValueError.UndeclaredValue.

Enum to enum

Cross-enum conversion is never implicit.

Integer-backed enums convert explicitly through a suitable integer representation:

let raw := int(SourceMode.Active)
let target := try TargetMode(raw)

String-backed enums use an explicit string intermediary.

Assignment, Equality and Arithmetic

Values of the same enum type use ordinary assignment rules:

let mut state := Status.Ready
state = Status.Waiting

No implicit assignment exists between:

  • an enum and its underlying integer or string type;
  • two different enum types.

Equality

if color == Color.Red {
    HandleRed()
}

Equality and inequality are defined between values of the same enum type. Comparison uses the semantic underlying enum value.

Aliases with the same underlying value compare equal.

No direct arithmetic or ordering

Enum values do not directly support arithmetic:

let next := status + 1    // invalid

They are not directly ordered:

if status < Status.Failed {    // invalid
}

String-backed enums likewise do not acquire string concatenation or ordering.

Convert explicitly to the underlying representation when numeric or textual operations are genuinely intended.

Match and Exhaustiveness

match understands enum domains directly.

Closed ordinary enum

enum Direction {
    North
    East
    South
    West
}

match direction {
    Direction.North => MoveNorth()
    Direction.East => MoveEast()
    Direction.South => MoveSouth()
    Direction.West => MoveWest()
}

This is exhaustive because every unique declared runtime value class is covered.

Numeric or string aliases count as one coverage class.

Open bit-backed enum

match mode {
    DeviceMode.Off => HandleOff()
    DeviceMode.Active => HandleActive()
    DeviceMode.Sleep => HandleSleep()
    _ => HandleUnknown()
}

Exhaustiveness for bit[N] is based on all 2^N bit patterns, not merely on declared names.

A catch-all is unnecessary only when explicit patterns already cover the complete bit domain.

Switch

An enum may be used as a normal switch subject.

switch status {
case Status.Ready:
    Start()

case Status.Waiting:
    Wait()

default:
    Stop()
}

switch uses normal value-comparison semantics. It does not become exhaustive merely because the subject is an enum.

Use match when exhaustive enum handling is part of the intended contract.

Foreign C Enums

A C ABI enum declared through the foreign interface is a separate category from both ordinary Sec enums and bit-backed hardware enums.

extern "C" type NativeStatus enum {
    ...
}

Its representation is selected by the active C ABI.

Its value domain is open over every raw value representable by that ABI enum representation, because foreign code may return values not named by the Sec declaration.

Code that needs exhaustive interpretation of a foreign enum therefore requires an unknown-value fallback where undeclared raw values remain possible.

Do not conflate the three models: ordinary Sec enum, hardware bit[N] enum, and foreign C ABI enum have different domain rules even when their machine representations happen to be similar.

Diagnostics and Tooling

Important enum diagnostics include:

  • duplicate enum member name;
  • invalid enum underlying type;
  • empty enum declaration;
  • initializer is not a constant expression;
  • initializer is not representable by the underlying type;
  • string-backed member requires an explicit initializer;
  • string-backed initializer is not a compile-time string constant;
  • iota used in a string-backed enum;
  • iota used outside enum member initialization;
  • multiple members marked default;
  • underlying value is not a declared member of a closed enum;
  • integer value does not fit a bit-backed enum width;
  • fallible enum conversion requires try;
  • non-exhaustive match for a closed enum;
  • non-exhaustive match for an open bit-backed enum.

Tooling should expose both the underlying representation and the enum-domain kind:

closed ordinary enum
open bit-backed enum

For open hardware enums, diagnostics and hover information should make clear that undeclared in-range bit patterns are valid runtime values.

Summary

  • An enum is a distinct nominal value type.
  • Enum members are typed enum constants in the enum namespace.
  • An ordinary enum defaults to underlying type int.
  • Ordinary enums may use explicit integer or string representations.
  • Integer-backed member initializers are compile-time integer expressions.
  • iota is the zero-based member position for integer-backed enum initialization.
  • The first omitted integer initializer is implicit iota.
  • Later omitted integer initializers repeat the preceding initializer expression.
  • An explicit numeric jump does not reset iota.
  • String-backed members require explicit compile-time string values.
  • iota does not apply to string-backed enums.
  • Duplicate underlying values are valid aliases.
  • Aliases with the same underlying value compare equal and form one match-coverage class.
  • Every valid non-empty enum is defaultable.
  • The first member is the default unless exactly one member is marked default.
  • Raw numeric zero has no special default status.
  • Ordinary integer- and string-backed enums are closed over declared value classes.
  • An ordinary closed enum may be marked as a concrete Sec error type.
  • Concrete error enums widen to error while preserving concrete identity.
  • Generic enums are supported and every concrete argument list creates a distinct nominal type.
  • Generic enum members remain payload-less; payload-bearing alternatives belong to unions.
  • Enums may be nested inside an owning impl.
  • Enums may have methods, properties and associated values through ordinary impl rules.
  • Associated values do not add enum members or alter the enum value domain.
  • bit and bit[N] enums are open over every representable bit pattern.
  • Undeclared in-range hardware bit patterns are valid unnamed enum values.
  • A bit-backed enum may type a register field when widths match exactly.
  • Enums do not implicitly convert to or from their underlying representation.
  • Runtime conversion into a closed enum is checked and requires try when failure cannot be ruled out.
  • Runtime conversion into a bit-backed enum fails only when the value does not fit the bit width.
  • Enum equality is defined within the same enum type.
  • Direct arithmetic, ordering and string concatenation are not enum operations.
  • match uses the enum's semantic domain for exhaustiveness.
  • switch may branch on enum values but is not enum-exhaustive.
  • Foreign C enums form a separate open-domain ABI category.