Programming Language

Nocter

A self-contained systems language built around simplicity, encapsulation, and foolproof design.

/spec/language/structs-and-enums.md

Structs and Enums

This chapter defines nominal struct and enum declarations, their value construction, and enum pattern binding behavior.

Structs and Value Construction

Struct values may be constructed with explicit named-field struct literals when the structural entry is accessible.

pub struct User {
    pub id: u64
    name: String
}
let user = User {
    id: 1,
    name: String.copy("alice"),
}

Rules:

  • Struct literal syntax is Type { field: value, ... }.
  • A struct may declare zero fields. Its structural literal is Type {} under the same visibility and construction-surface rules as any other struct.
  • Struct literal fields are comma-delimited and may use one trailing comma on any layout.
  • The type in a struct literal must name a struct type. A generic owner may use complete explicit type arguments or infer all of them under Generic Owner Arguments.
  • Every field must be initialized exactly once.
  • Field order in the literal is free.
  • Unknown fields are compile errors.
  • Duplicate fields are compile errors.
  • Field initializer expressions are evaluated left to right in the order written in the literal.
  • Field initializer expressions follow normal ownership, move, copy, borrow, and postfix ? rules.
  • If a later field initializer fails through postfix ?, already initialized owned field values are dropped in reverse initialization order before the failure propagates.
  • Private fields may be initialized only in their authored source and sources that directly see it.
  • Public fields may be initialized from other modules.
  • Construction entries cannot be overloaded.
  • Field default values, struct update syntax, positional structs, and tuple structs are not supported.
  • A construct declaration groups type-owned construction functions and typed literals. Each member has its own visibility. The declaration does not change structural construction visibility. See Construction Surfaces.
  • Names such as new, init, and create are ordinary construction-function names when declared inside construct. The compiler does not special-case them.

When initialization logic or validation is needed, place a public construction function in the type's construct declaration.

construct User {
    pub func create(id: u64, name: String): Self {
        return User {
            id: id,
            name: move name,
        }
    }
}

Outside the private field's direct source-access boundary, a struct with private fields can be created only through public APIs exposed by its module.

let user = User.create(1, String.copy("alice"))

Enums and Variant Construction

Enums represent finite variants and may carry data.

enum AppError {
    missing_path
    open_failed(path: &str)
}

Rules:

  • An enum must declare at least one variant. A zero-variant enum is invalid and does not define a nominal uninhabited value type.
  • Enum variant names use snake_case.
  • Variants may carry zero or more payload values.
  • Variant payload declarations and constructor arguments are comma-delimited and may use one trailing comma on any layout.
  • Payloadless variants are constructed as EnumName.variant_name.
  • Payload variants are constructed as EnumName.variant_name(args...).
  • Every enum uses a u8 ABI tag and must declare between 1 and 256 variants, inclusive, whether or not its variants carry payloads.
  • Variant construction requires the payload arity and types to match the variant declaration.
  • Variant payload arguments are evaluated left to right.
  • Variant constructors are qualified with the enum name, such as AppError.open_failed(path).
  • Variant constructors must be qualified.
  • A generic enum owner may use complete explicit type arguments or infer all of them under Generic Owner Arguments.
  • Variant constructors are not ordinary functions and are not magic identifier names; they are generated by the enum declaration.
  • Enum variants and construction functions share the type member namespace. Defining a construction function with the same member name as a variant is a compile error.
  • If an enum is public, its variants are public.
  • Per-variant visibility is not supported.

Examples:

let state = ScanState.inside_word
let error = AppError.open_failed(path)

match is the control-flow form for enum pattern matching.

match error {
    AppError.missing_path {
        ...
    }
    AppError.open_failed(path) {
        ...
    }
    _ {
        ...
    }
}

Enum patterns are shallow and positional. Their source form is centralized under Control Expressions and Enum Patterns.

The payload list uses the common comma-delimited-list grammar. A payloadless variant uses the first form. A payload-bearing variant uses the second form and supplies exactly one slot for every declared payload field.

enum Pair {
    values(left: String, right: String)
}

match &pair {
    Pair.values(_, right) {
        inspect(right) // right: &String
    }
}

The pattern target chooses how payload names are bound. Borrowed matching inspects an enum without extracting its payload:

var message = next_message()

match &message {
    Message.text(text) {
        inspect(text) // text: &String
    }
    _ {
        ...
    }
}

match &+message {
    Message.text(text) {
        text.clear() // text: &+String
    }
    _ {
        ...
    }
}

match move message {
    Message.text(text) {
        consume(move text) // text: String
    }
    _ {
        ...
    }
}

Rules:

  • Match arms use Pattern { ... }.
  • A variant pattern must use the exact enum qualifier and variant name selected by the target enum type.
  • Payload pattern slots are positional and their count must equal the variant payload arity.
  • An identifier slot introduces one branch-local binding for the payload field at that position. It does not need to repeat the field's declaration name.
  • _ always occupies exactly one payload position and introduces no binding. Ignoring every field of a multi-payload variant requires one _ for each field, such as Pair.values(_, _).
  • Pair.values(_) is therefore an arity error when values has two payload fields. _ never abbreviates an entire payload list.
  • Nested patterns, literal patterns, binding modifiers, field-name patterns, and rest patterns are not supported.
  • Enum variant patterns are tag patterns, not value refinements. Payload binding names and _ control projection only.
  • Every enum payload field may use any sized type that is valid as a struct field. Construction, local storage, arguments, returns, assignment, optional/fallible wrapping, and pattern matching apply recursively to payload aggregates without a separate runtime type allowlist.
  • A pattern target whose type is Enum, &Enum, or &+Enum selects one of the binding modes in the table below. Pattern matching dereferences a borrowed target only for tag inspection and payload projection; it does not introduce a general implicit dereference conversion.
Pattern targetPayload name typeEffect on the target
New owned enum temporarydeclared payload typeConsumes the temporary
Existing enum place without movedeclared payload type, only when that payload is copyableCopies the named payload; retains the enum place
Readonly borrow expression of type &Enum&PayloadRetains the enum and creates a readonly payload borrow
Readwrite borrow expression of type &+Enum&+PayloadRetains the enum and creates an exclusive readwrite payload borrow
move placedeclared payload typeConsumes the enum place
  • The borrowed modes apply both to an explicit target such as match &value or match &+value and to any target expression already typed as &Enum or &+Enum. Using an existing borrow as a pattern target is a use of that borrow, not an ownership transfer.
  • Every payload name in one pattern uses the target's binding mode. A borrowed target therefore binds even a copyable payload as &Payload or &+Payload; it never performs a hidden payload copy. Code that needs owned copies matches an existing enum place without a borrow.
  • Creating an &+Enum target follows the ordinary writable-place and exclusivity rules. Payload borrows derived from a borrowed target carry the target borrow's provenance and keep that borrow active through their last source-level use. A payload borrow may be returned or stored only when the ordinary borrow and provenance rules permit it.
  • An existing enum place without move may bind copyable payloads even when another variant makes the enum type move-only. Naming a move-only payload from that target is an error. A payloadless pattern or _ payload may still inspect the tag without consuming the place.
  • Binding owned move-only payloads from an existing local, parameter, or named struct field requires an explicit move target, such as match move result or match move holder.result. The ordinary move-place restrictions still apply; indexes, dereferences, and computed projections are not move sources.
  • A newly produced owned enum temporary, including a call result, variant constructor, control expression result, or value produced through postfix ?, postfix !, catch, or otherwise, is already owned by the pattern operation and does not use move.
  • An owned target is consumed as a whole into pattern-operation temporary storage before arm execution. Named payload bindings assume their fields' drop obligations. Unnamed fields and the complete active payload in a fallback arm remain in that storage. Its residual initialized fields are dropped by the ordinary statement-temporary rules after the selected arm is evaluated, or by early-exit cleanup if the arm exits the statement. No field of a consumed enum is dropped twice.
  • If an owned enum has a type-owned drop declaration and an explicit arm transfers any named move-only payload, the drop body observes the complete enum exactly once after tag selection and before any payload leaves pattern-operation storage. The residual cleanup later drops only unnamed initialized payload fields and does not call the type-owned drop body again.
  • If such an arm binds only copyable payloads, those bindings copy their values and the complete enum remains in pattern-operation storage. Its ordinary residual cleanup invokes the type-owned drop body and then drops the active payload. Fallback and implicit non-match paths likewise keep the complete enum until ordinary cleanup.
  • Enum cleanup reads the active tag and drops only initialized fields of that variant. Fields drop in reverse payload declaration order and recursively use the same struct, enum, fixed-array, and outcome cleanup rules. Fixed-array elements drop in reverse index order.
  • Scope-end cleanup, parameter cleanup, explicit discard initializers, call-result temporaries, assignment replacement, and partial control-flow cleanup all use the same active-variant rule.
  • Payload names in a pattern are bound only inside that arm block.
  • _ inside a payload pattern, such as AppError.open_failed(_), occupies exactly one declared payload position without introducing a binding. For a consumed target, that unnamed owned field remains under pattern-operation temporary cleanup. For a borrowed or retained target, the field is neither copied nor moved.
  • _ by itself is valid only as the match fallback arm. It is not a valid if is pattern.

Selection order, fallback placement, exhaustiveness, result typing, termination, and single evaluation of the target belong to Control Flow.

Example value selection:

return match error {
    AppError.missing_path { missing_code() }
    AppError.open_failed(path) { code_for(path) }
    _ { unknown_code() }
}

if enum_expr is Pattern checks one enum pattern.

if error is AppError.open_failed(path) {
    report(path)
} else if error is AppError.read_failed(path) {
    report(path)
} else {
    report_other(error)
}

Rules:

  • if enum_expr is Pattern uses the same enum pattern syntax as match.
  • if pattern targets use the same owned, copied, readonly-borrowed, readwrite-borrowed, and moved binding modes as match.
  • Payload names are bound only inside the then body.
  • if enum_expr is Enum.variant(_) checks only the variant of a one-payload enum case and ignores that payload without introducing a binding. A multi-payload variant requires one slot per field.
  • Payload names are not available in else or later else if branches.
  • if is does not apply to fallible values T!.
  • if is does not apply to optional values T?.