hd-lang

#Data Types and Enums

Status: language specification draft.

This chapter defines structs and enums.

  1. data.kind.structA struct is a nominal product type with value semantics.
  2. data.kind.enumAn enum is a nominal sum type.
  3. data.kind.no-classNeither is a class, and neither creates an inheritance hierarchy.
struct Point:
    x: i32
    y: i32

enum Shape:
    Dot
    Circle(radius: f64)

#Struct Declarations

A struct declaration defines a struct with named, typed fields. A struct value is copied, never shared.

struct Vec2:
    x: f64
    y: f64 = 0.0

fn demo() -> f64:
    let mut a = Vec2 { x: 1.0 }
    b := a      # b is a copy of a
    a.x = 5.0   # changes a only
    b.x         # 1.0
  1. data.struct.declstruct Name: declares a struct. Its body holds named fields, defaults, visibility, decorators, and generic parameters.
  2. data.decl.nominalStruct identity is nominal: two declarations with the same fields introduce different types.
  3. data.struct.valueA struct is a value type, as Value Types defines.
  4. data.struct.receiver-formsA method of a struct takes self or mut self, its own read-only or changeable copy of the receiver, or &self, &mut self, *self, or *mut self, a reference to the caller's value.
  5. data.struct.mut-selfA struct method's mut self changes its copy only. The caller's value is unchanged unless the method returns the copy and the caller keeps it.
  6. data.struct.embedA struct may embed a struct, by the rules of Data Embedding for names, limits, and member promotion. An embedded field is a value field, held inline.
  7. data.struct.embed.plainA struct literal fills an embedded field as it fills a named field, as in Post { Timestamps: ts }, and post.Timestamps = ts assigns it. Both copy the value, as every struct copy does.
  8. data.struct.no-recursionA struct that contains itself by value is an error. Error: recursive-value-type.
  9. data.struct.containsA value contains the values of its struct fields, enum payloads, tuple elements, optional payload, and newtype base, and what those contain.
  10. data.struct.contains.endsA collection, a trait value, or a function value holds a reference, so containment ends there.
  11. data.struct.contains.referenceA counted reference, *T or *mut T, also ends containment.
  12. data.struct.no-recursion.fixThe recursive-value-type error of a struct has a fix-it that writes * before the type of each field that contains the struct.
Error example
struct Vec2:
    x: f64
    y: f64

enum Unit:
    Metre
    Foot

struct Pair:
    Vec2          # valid: an embedded struct
    Unit          # error: embedded-non-data
    weight: f64

struct Chain:
    next: Chain?  # error: recursive-value-type
impl Vec2:
    fn moved(mut self, dx: f64) -> Vec2:
        self.x += dx
        self

Why. A struct holds its fields inline, so one that contained itself would have no finite size. Recursion goes through a reference or a collection, as next: *Chain? and next: List[Chain] do.

See also: Value Types, Writable Places.

#Fields

A struct declares named, typed fields:

struct User:
    id: string
    email: string
    display_name: string? = .None
  1. data.field.uniqueField names must be unique within the struct. Error: duplicate-field. The named parameters of an enum header, and the named payload parameters of one variant, are unique in the same way.
  2. data.field.typedEvery field has an explicit type.
  3. data.field.no-mut-modifierFields have no standalone mut modifier: mut friend: User is an error. Error: mutable-field-modifier.
  4. data.field.mut-typefriend: *mut User declares a field whose type grants mutable access through that reference.
  5. data.field.embedded-no-mutAn embedded field is written without mut: Base embeds Base, and mut Base is an error. Error: mutable-field-modifier.
Error example
struct User:
    mut name: string    # error: mutable-field-modifier
    mut Base            # error: mutable-field-modifier
    friend: *mut User   # valid: the type grants mutable access

Why. Access to an embedded field already follows its container.

#Field Defaults

  1. data.default.allowedAn ordinary named field may have a default expression.
  2. data.default.typeThe default must be assignable to the declared field type.
  3. data.default.requirement-freeThe default must obey the same requirement-free rule as a function-parameter default.
  4. data.default.evalA default is evaluated separately for each construction, not when the struct is declared.
  5. data.default.scopeA default sees the declaration's lexical scope.
  6. data.default.no-field-bindingA default does not implicitly bind other fields of the new value.
  7. data.default.embeddedEmbedded fields have no default syntax.
struct Config:
    retries: i32 = 3
    name: string

fn demo() -> i32:
    Config { name: "shop" }.retries   # 3: the default fills the missing field

See also: Default Values.

#Field Visibility

  1. data.vis.privateNamed fields are module-private unless individually marked pub.
  2. data.vis.type-not-fieldsA public struct does not make its unmarked fields public.
  3. data.vis.embedded-publicAn embedded field takes no marker and is always public: it is visible wherever its outer type is.
  4. data.vis.literalIn another module, a struct literal may construct the type only when all its fields are public. Any other such literal is an error. Error: private-member.
  5. data.vis.private-fieldsIn another module, private fields cannot be named, initialized, or carried through a copy-update literal. Doing so is an error. Error: private-member.
  6. data.vis.factoryA public factory function can construct a value with private fields inside the defining module.

Because an embedded field is public, embedding a module-private struct in a public struct leaks it, as module.vis.signature.coverage states:

Error example
struct Timestamps:
    created_at: i32

pub struct Post:
    Timestamps  # error: private-type-leak
    title: string

Why. An embedded field is always public: it is visible wherever its outer type is.

See also: Member Resolution, Public Uses And Visibility.

#Fieldless Data Types

A struct with no fields is written with pass:

struct Unit: pass

unit := Unit {}
  1. data.empty.passAn empty struct uses pass as its body.
  2. data.empty.no-allocationConstructing a value of a fieldless struct allocates nothing.

#Recursive Data Types

  1. data.recursive.through-referenceA struct may refer to itself through a reference, an optional reference, or a collection, as next: *Chain? and replies: List[Comment] do.
  2. data.recursive.mutualModule-level structs may also be mutually recursive through such links.
  3. data.recursive.localLocal structs follow declaration-point visibility and cannot refer to a later local declaration.
  4. data.recursive.required-fieldsRecursion does not imply optionality: a program must still provide a value for every required field during construction.
struct Chain:
    value: i32
    next: *Chain?

Note. A recursive graph therefore normally includes an optional, enum, list, or another finite base case.

See also: data.struct.no-recursion, Generic And Recursive Enums.

#Construction And Access

A typed brace literal constructs a struct value, and value.field accesses a field.

user := User {
    id: "user_123",
    email: "ada@example.com",
    display_name: "Ada",
}

#Data Literals

  1. data.literal.formStruct values use typed brace literals.
  2. data.literal.requiredEach field without a default must be initialized exactly once. Omitting one is an error. Error: missing-required-field.
  3. data.literal.defaultedA field with a default may be omitted or supplied explicitly.
  4. data.literal.unknownAn unknown field is a compile-time error. Error: unknown-field.
  5. data.literal.duplicateA duplicate field is a compile-time error. Error: duplicate-field.
  6. data.literal.any-orderFields may appear in any order.
  7. data.literal.eval-explicitExplicit field expressions are evaluated in source order.
  8. data.literal.eval-defaultsThen defaults for omitted fields are evaluated in field declaration order.
Error example
missing := User { id: "user_123" }                       # error: missing-required-field
unknown := User { id: "u", email: "e", nickname: "Ada" } # error: unknown-field
twice := User { id: "u", id: "v", email: "e" }           # error: duplicate-field

#Field Shorthand

  1. data.literal.shorthandA field may be written as its bare name when a binding of that name is in scope: Point { x, y } means Point { x: x, y: y }.
  2. data.literal.shorthand.mixedShorthand fields and field: value fields may be mixed in one literal, as in Point { x, y: 0 }.
  3. data.literal.shorthand.rulesA shorthand field follows every rule of the field: value it stands for, so it counts toward data.literal.required and data.literal.duplicate.
  4. data.literal.shorthand.unknownA shorthand name that resolves to no binding in scope is an error, as for any unknown value name. Error: unknown-name.
Error example
struct Point:
    x: i32
    y: i32

fn place(x: i32, y: i32) -> (Point, Point):
    (Point { x, y }, Point { x, y: 0 })

fn broken(x: i32) -> Point:
    Point { x, y }  # error: unknown-name

Why. A data pattern already binds Point { x, y } field by field, so the literal builds with the same spelling it destructures with.

See also: Data Expressions.

#Copy-Update Literals

A copy-update literal builds a new value from an existing one:

renamed := User {
    ...user,
    display_name: "Ada Lovelace",
}
  1. data.update.spreadCopy-update construction uses one leading spread.
  2. data.update.same-typeThe spread has the same struct type.
  3. data.update.supplies-allThe spread supplies every field.
  4. data.update.replaceExplicit fields replace copied values.
  5. data.update.new-valueA copy-update literal creates a new struct value; it does not mutate the spread source.
  6. data.update.no-defaultsBecause the spread supplies every field, copy-update does not evaluate defaults for unlisted fields.
  7. data.update.shorthandAn explicit replacement may use field shorthand, as in Point { ...origin, x }, which means Point { ...origin, x: x }.

See also: Data Embedding, Data Expressions.

#Field Access

  1. data.access.syntaxField access uses value.field.
  2. data.access.writable-placeAssigning a field, or changing its value in place, requires a writable place, as Writable Places states.
  3. data.access.assignmentDirect assignment to a visible field enforces its declared type, not arbitrary validation or cross-field invariants.
  4. data.access.graphThe language permits shared mutable children through *mut references, and does not guarantee invariants over the entire reachable object graph.
  5. data.access.other-moduleIn another module, field access reaches only public fields; member lookup skips the others.
struct User:
    pub name: string
    email: string

fn rename(user: &mut User, name: string) -> void:
    user.name = name   # a write through a &mut reference

Note. Keep fields private and expose controlled methods when writes to those fields must preserve such invariants. This does not control other mutable references to a value that a private field refers to.

See also: Writable Places, Data Embedding, Member Resolution.

#Data Patterns

  1. data.pattern.formStruct values may be destructured in match patterns using the same StructName { ... } form.
  2. data.pattern.subsetThe pattern may mention any subset of visible fields; omitted fields are not tested.
  3. data.pattern.fieldsWithin the braces, field binds the field value and field: pattern applies a nested pattern.
struct Point:
    x: i32
    y: i32

fn on_axis(p: Point) -> bool:
    match p:
        Point { x: 0 } => true
        Point { y: 0 } => true
        _ => false

See also: Match Expressions.

#Data Embedding

A bare type-name member is an embedded field, which embeds another struct inline.

struct Post:
    Timestamps
    id: string
    title: string

A struct literal fills the embedded field as it fills a named field:

post := Post {
    Timestamps: Timestamps {
        created_at: 1700000000,
        updated_at: 1700000000,
    },
    id: "post_123",
    title: "Hello",
}

#Embedded Field Names

  1. data.embed.memberA bare type-name member embeds another struct, possibly with type arguments. The embedded field's name, and its key in a struct literal, is the type's final name without type arguments: Box[T] is the field Box.
  2. data.struct.embed.struct-onlyA struct's embedded field must name a struct, a generic struct such as Box[T], or a transparent alias that resolves to one. Embedding any other type is an error. That covers an enum, a newtype, a trait value type, Any, a builtin or collection type, a function type, and a type parameter. Error: embedded-non-data.
  3. data.embed.uniqueThe name must be unique among the outer struct's fields. A duplicate name that involves an embedded field is an error. Error: duplicate-embedded-field.
Error example
struct Storage:
    Box[i32]
    Box[string]  # error: duplicate-embedded-field
Error example
struct Holder[T]:
    JobStatus  # error: embedded-non-data
    List[i32]  # error: embedded-non-data
    T          # error: embedded-non-data

#Embedding Limits

Embedding is limited in width and depth, and a struct never embeds itself.

Rule Limit Error Reported on
data.embed.widthWidth A struct may declare at most three embedded fields. too-many-embedded-fields the fourth embedded field
data.embed.depthDepth Embedding chains may be at most three levels deep: C may embed P1, which embeds P2, which embeds P3. A type that reaches a cycle without being part of it gets no embedding-cycle; its depth counts along chains that never repeat a type. embedding-too-deep the embedded field that begins the first chain that reaches depth 4
data.embed.depth.selfCycle A struct that embeds itself, directly or through other types, is an error, in place of embedding-too-deep. embedding-cycle the embedded field that begins the cycle, in the cycle's first declared type
Error example
struct Post:
    Created
    Updated
    Owned
    Tagged  # error: too-many-embedded-fields
    title: string

Here Site reaches Stamp at depth 4, one level too deep:

Error example
struct Audit:
    Stamp
    by: string

struct Record:
    Audit
    id: string

struct Page:
    Record
    title: string

struct Site:
    Page  # error: embedding-too-deep
    host: string
Error example
struct Node:
    Edge  # error: embedding-cycle
    id: string

struct Edge:
    Node
    weight: i32

Note. For the error revamp: embedding-too-deep is reported at the declaration of every struct that reaches an embedded struct at depth 4. Its message shows the chain, as in C > P1 > P2 > P3 > P4. The embedding-cycle message shows the cycle path, as in Node > Edge > Node.

Note. Together the two limits bound a struct's embedding tree to at most 3 + 9 + 27 = 39 embedded structs.1

#Embedded Field Variance

For variance, an embedded field's type is an invariant position, as Variance states.

Error example
struct Box[+T]:
    value: T

struct Holder[+T]:
    Box[T]  # error: invalid-variance

Why. Access through an embedded field follows the container.

#Member Promotion

  1. data.promote.membersEmbedding promotes the pub fields and pub inherent methods of an embedded type for convenient access, as Member Resolution defines.

Each fact about promotion is stated once, in chapter 03 or chapter 09:

Fact Rule
Only the pub fields and pub inherent methods of an embedded struct, at any depth, are promoted; trait methods never are. names.promote.member
An embedded struct's own embedded field is also promoted; it takes no pub marker but is always public. names.promote.embedded
For each name, the shallowest member hides deeper ones, so a pub own member hides a promoted one. names.hide.depth
A private own member with a promoted member's name is ambiguous-promoted-member. names.conflict.private-shadow
Two members with one name at the smallest depth are ambiguous-promoted-member at the declaration. names.conflict.error
A promoted member means its explicit path, with the embedded field as receiver: there is no overriding. names.promoted.path
A trait method of the receiver beside a promoted method is ambiguous-method. names.method-lookup.ambiguous
Embedding grants no trait conformance. trait.embed.no-conformance
A promoted method never fills a trait method. trait.impl.fill.never
Error example
struct Left:
    pub id: string

struct Right:
    pub id: string

struct LeftBox:
    Left

struct RightBox:
    Right

struct Record:
    LeftBox
    RightBox  # error: ambiguous-promoted-member

struct Base:
    pub id: string

struct Entry:
    Base
    id: string  # error: ambiguous-promoted-member

See also: Member Resolution, which defines which field x.name or method x.name(args) selects.

#Composition, Not Subtyping

Embedding is composition, not subtyping. The outer struct is a different nominal type, so it is not assignable to the embedded type. A promoted method runs as the embedded type's own method, and embedding grants no trait conformance.

Note. Conformance through an embedded field is written explicitly, with by, as in impl Describe for Service by Logger.

See also: Embedding And Trait Satisfaction, Trait Delegation.

#Embedded Field Metadata

An embedded field is a field, so it accepts the same prefix metadata decorators as a named field, as annot.target.kind.field states. Its name is the final type name, and the metadata stays on the field: promoted members do not carry it.

#Enum Declarations

An enum declaration lists its variants:

enum JobStatus:
    Queued
    Running
    Succeeded
    Failed
  1. data.enum.closedAn enum defines a closed set of variants.
  2. data.enum.uniqueVariant names must be unique within the enum. Error: duplicate-variant.
  3. data.enum.qualifiedThe qualified form is JobStatus.Queued.
  4. data.enum.shorthand.Queued is also valid where an expected type fixes JobStatus.
  5. data.enum.shorthand.other-enumA matching name in another enum does not create ambiguity when that expected type is known.
  6. data.enum.shorthand.needs-typeA unique name across the program does not make the shorthand valid without an expected enum type.
  7. data.enum.unknown-variantA qualified or shorthand variant name that names no variant of the enum is an error. Error: unknown-variant.

#Variant Payloads

Variants may carry payloads:

enum ToolError:
    NotFound(resource: string)
    Unauthorized(reason: string)
    RateLimited(retry_after_ms: i32)
  1. data.enum.payloadA variant may carry payload parameters.
  2. data.enum.payload.orderIn a variant constructor call and in a variant pattern, positional arguments and sub-patterns come before name= ones. Function calls follow the same rule, fn.arg.positional-first.
  3. data.enum.payload.typedEach payload type is explicit.
  4. data.enum.payload.largeLarge payloads should use a separate struct rather than a nested field block.
  5. data.enum.payload.no-field-blocksVariant field blocks are not part of the language.
  6. data.enum.immutableAn enum value never changes once built: its variant and its payload values are fixed at construction.
  7. data.enum.immutable.shallowThe rule is shallow. A payload of type *mut U still refers to a place, which may change through that reference.
  8. data.enum.no-identityAn enum value has no identity, so every enum type implements AnyVal, as types.sealed.anyval-members states.
  9. data.enum.no-identity.isis with an enum operand is an error. Error: identity-requires-references.

#Variant Construction

A variant is constructed with call syntax:

error := ToolError.NotFound(resource="user_123")
  1. data.enum.construct.callVariant construction follows function-call conventions: positional arguments come before named arguments.
  2. data.enum.construct.unknownA named argument that names no payload field of the variant is an error. Error: unknown-field.
  3. data.enum.construct.call-codesOther argument errors use the call codes in Calls, such as duplicate-argument.
Error example
enum Pair:
    Value(first: i32, second: i32)

fn make() -> Pair: Pair.Value(missing=1, first=2)  # error: unknown-field
fn again() -> Pair: Pair.Value(1, first=2)         # error: duplicate-argument

#Variant Constructors As Function Values

Some variant constructors are function values:

enum FsError:
    NotFound(path: string)

enum SyncError:
    Fs(error: FsError)
    Retry(attempt: i32, error: FsError)

fn wrap_all(errors: List[FsError], wrap: fn(FsError) -> SyncError) -> List[SyncError]:
    [for error in errors => wrap(error)]

wrapped := wrap_all([FsError.NotFound("a.txt")], SyncError.Fs)
retry := fn(error: FsError) -> SyncError: SyncError.Retry(1, error)
  1. data.enum.fn-valueA variant constructor with exactly one payload field is a function value when it is written without an argument clause.
  2. data.enum.fn-value.typeToolError.NotFound has type fn(string) -> ToolError: one positional parameter of the payload type, the enum as result, no suspension, and the empty requirement row.
  3. data.enum.fn-value.useIt may be passed wherever that function type is expected, such as to a standard-library map_err in result.map_err(SyncError.Fs).
  4. data.enum.fn-value.positionalThe payload's field name and positional or named declaration do not matter, because function values take positional arguments only.
  5. data.enum.fn-value.generic-ruleFor a generic enum, or a variant with its own generic parameters, the value follows the rule for generic function values.
  6. data.enum.fn-value.generic-argumentPassed as a call argument, it takes its type arguments from the call. So result.map_err(TaskError.Failed) on a Result[T, FsError] gives TaskError[FsError].
  7. data.enum.fn-value.generic-unsolvedA generic parameter that remains unsolved is an error. Error: cannot-infer-type.
  8. data.enum.fn-value.shorthandThe contextual shorthand .Variant still needs an expected enum type, so it is never a function value. Using it as one is an error. Error: missing-contextual-enum-type.
  9. data.enum.fn-value.callCalling the value constructs the variant, exactly as calling the constructor does.
  10. data.enum.fn-value.multipleA variant constructor with two or more payload fields is not a function value and must be called.
  11. data.enum.fn-value.unsaturatedUsing one without an argument clause is an error. Error: unsaturated-enum-constructor.
  12. data.enum.fn-value.closureAn explicit closure passes its construction as a function value.
  13. data.enum.fn-value.payload-freeA payload-free variant, including one whose declaration initializes shared enum data, is an enum value and is selected without ().
Error example
fn apply(make: fn(i32, FsError) -> SyncError) -> SyncError:
    make(1, FsError.NotFound("a.txt"))

fn retry() -> SyncError:
    apply(SyncError.Retry)  # error: unsaturated-enum-constructor

A generic variant constructor takes its type arguments from the call:

enum FsError:
    NotFound(path: string)

enum TaskError[E]:
    Failed(error: E)
    Cancelled

fn wrap_error[T, E, F](result: Result[T, E], wrap: fn(E) -> F) -> Result[T, F]:
    match result:
        .Ok(value) => .Ok(value)
        .Err(error) => .Err(wrap(error))

fn read(path: string) -> Result[string, FsError]:
    .Err(FsError.NotFound(path))

wrapped := wrap_error(read("a.txt"), TaskError.Failed)
Error example
enum Either[L, R]:
    Left(value: L)
    Right(value: R)

fn wrap_all[T, F](values: List[T], wrap: fn(T) -> F) -> List[F]:
    [for value in values => wrap(value)]

fn main() -> void:
    lefts := wrap_all([1, 2], Either.Left)  # error: cannot-infer-type
Open in playground

In the second example, nothing determines R. An expected type such as let lefts: List[Either[i32, string]] would solve it.

See also: Function Types And Values.

#Shared Enum Constructor Data

Enums can declare constructor data that every variant shares:

enum StatusCode(i32):
    Ok -> StatusCode(200)
    NotFound -> StatusCode(404)

enum HttpStatus(code: i32, phrase: string, retryable: bool = false):
    Ok -> HttpStatus(200, phrase="OK")
    NotFound -> HttpStatus(404, phrase="Not Found")
    ServiceUnavailable -> HttpStatus(503, phrase="Service Unavailable", retryable=true)
  1. data.shared.declareAn enum may declare data shared by all variants.
  2. data.shared.parametersConstructor parameters may be unnamed or named.
  3. data.shared.constructorEach variant with shared enum data must provide its enum constructor expression after ->. A variant without one is an error. Error: missing-required-field.
  4. data.shared.ownerThe name after -> must be the enclosing enum. Any other name is an error. Error: variant-result-owner.
  5. data.shared.argumentsThe constructor call follows ordinary positional/named argument ordering and must initialize each shared parameter without a default. Error: missing-required-field.
  6. data.shared.per-variantShared constructor data belongs to the variant, not to each value: every value of one variant has the same shared data.
  7. data.shared.module-initEach variant's constructor expression is evaluated once at run time during its declaring module's initialization.
  8. data.shared.module-init.orderThese evaluations join that module's top-level initialization under module.init.cycle and Order Inside A Cycle.
  9. data.shared.module-init.empty-rowThe expression must satisfy module.init.requirement-free.non-script, and it may call any requirement-free function.
  10. data.shared.module-init.effectsIts writes to module state occur during initialization.
  11. data.shared.module-init.not-lazyThe expression is neither lazy nor compile-time evaluation.
  12. data.shared.no-payloadThe variant's payload parameters are not in scope in its constructor expression.
  13. data.shared.not-storedShared data is stored once per variant and never in an enum value, so it adds nothing to a value's size.
  14. data.shared.generic-copyA generic enum's shared data has one copy per variant of the declaration, not one per instantiation: Tagged[i32].A and Tagged[string].A read the same copy.
  15. data.shared.type-parameterA shared parameter whose type names a type parameter of the enum is an error, because one copy has no single type. Error: shared-data-type-parameter.

Why. Shared data describes a variant, like a Java or Kotlin enum constant's constructor arguments. Data that differs from value to value belongs in each variant's payload or in a wrapper struct.

#Shared Parameter Defaults

  1. data.shared.defaultA shared parameter may declare a default expression.
  2. data.shared.default.orderAfter the first defaulted parameter, every following shared parameter must also have a default, as with function parameters. A later parameter without one is an error. Error: default-order.
  3. data.shared.default.empty-rowThe default must satisfy data.shared.module-init.empty-row, as a function-parameter or struct-field default does.
  4. data.shared.default.eval-onceWhen omitted, the default is evaluated once for the variant during the variant's constructor call at module initialization.
  5. data.shared.default.eval-orderWithin one variant's constructor call, explicit argument expressions are evaluated first, then omitted defaults in parameter declaration order.
  6. data.shared.default.scopeA default may refer to earlier named shared parameters but not later ones.
Error example
enum Status(code: i32 = 0, phrase: string):  # error: default-order
    Unknown -> Status(phrase="unknown")

#Shared Fields

Shared data is read as fields of the enum value:

code := StatusCode.NotFound._0
phrase := HttpStatus.NotFound.phrase
  1. data.shared.value-readEvery enum value exposes its variant's shared data as fields.
  2. data.shared.named-fieldA named constructor parameter is available as a field on the enum value.
  3. data.shared.underscore-fieldAn unnamed parameter uses zero-based tuple-style access, spelled _0, _1, and so on, as in StatusCode.NotFound._0.
  4. data.shared.read-onlyA shared field is read-only: assigning one is an error, even through a &mut reference or a let mut binding. Error: invalid-assignment-target.
  5. data.shared.no-placeA shared field is no place, so a change to its value in place, through a method call or an element store, is an error. Error: not-writable.
  6. data.shared.payload-fieldsVariant payload fields remain available through pattern matching rather than direct field access, because they do not exist on every variant. A direct access is an error. Error: unknown-field.
  7. data.shared.payload-namesWithin one variant, a named payload parameter must not duplicate a named shared constructor parameter, nor another named payload parameter of that variant. Error: duplicate-field.
  8. data.shared.payload-defaultsVariant payload parameters do not have defaults; writing one is an error. Error: syntax-error.
Error example
enum HttpStatus(code: i32, phrase: string):
    Ok -> HttpStatus(200, phrase="OK")
    Moved(target: string) -> HttpStatus(301, phrase=target)  # error: unknown-name

fn rename(status: &mut HttpStatus) -> void:
    status.phrase = "Fine"  # error: invalid-assignment-target

#Generic And Recursive Enums

This section defines generic and recursive enums.

enum Maybe[T]:
    Some(value: T)
    None

enum Tree[T]:
    Leaf(value: T)
    Branch(left: *Tree[T], right: *Tree[T])
  1. data.enum.generic.paramsAn enum may declare generic parameters.
  2. data.enum.generic.instanceEvery variant constructs the enclosing enum instantiated with the declaration's type arguments.
  3. data.enum.recursiveAn enum may refer to itself through a reference, an optional reference, or a collection, as Branch(left: *Tree[T], right: *Tree[T]) does.
  4. data.enum.no-recursionAn enum that contains itself by value, as data.struct.contains defines containment, is an error. Error: recursive-value-type.
  5. data.enum.no-recursion.fixThe error has a fix-it that writes * before the type of each payload field or shared field that contains the enum.
  6. data.enum.no-recursion.groupStructs and enums that contain one another, each through the next, are one recursive-value-type error, reported at one of them.
  7. data.enum.no-boxingNo value is boxed implicitly: each * in a recursive enum's types is written, and & on a new value allocates its object.
Error example
enum Tree:
    Leaf(value: i64)
    Node(left: Tree, right: Tree)  # error: recursive-value-type

enum Chain:
    Link(next: Chain?)  # error: recursive-value-type

enum Branch:
    Fork(forest: Forest)  # error: recursive-value-type

struct Forest:
    branch: Branch

Why. An enum holds its payloads inline, as a struct holds its fields, so one that contained itself would have no finite size. Writing the * keeps every allocation visible, as Go's pointers and Rust's Box do.

Why. hd has no generalized algebraic data types. A typed request and its response pair through a trait with an associated type, and a typed interpreter uses a runtime value enum or a trait.

See also: Variant result types.

#Matching Enums

A match inspects an enum value:

match error:
    ToolError.NotFound(resource) => resource
    ToolError.Unauthorized(reason) => reason
    ToolError.RateLimited(retry_after_ms) => retry_after_ms.to_string()
fn queued() -> JobStatus: .Queued

fn label(status: JobStatus) -> string:
    match status:
        .Queued => "queued"
        .Running => "running"
        .Succeeded => "succeeded"
        .Failed => "failed"
  1. data.match.exhaustiveEnum values are inspected with exhaustive match.
  2. data.match.variant-patternsVariant patterns may be qualified, or use .Variant when the matched value fixes their enum.
  3. data.match.shorthandThe shorthand also works for payload construction and nested variant patterns when their expected enum type is fixed.
  4. data.match.no-inferenceThe shorthand is not a way to infer an enum from the variant spelling alone: status := .Queued has no contextual enum type and is an error. Error: missing-contextual-enum-type.
Error example
status := .Queued  # error: missing-contextual-enum-type

#Payload Patterns

Payload patterns destructure a variant's payload:

Expr.Add(l, r)
Expr.Sub(left=l, right=r)
Expr.Scale(value, factor=2)
  1. data.match.payload.parenthesesPayload patterns use parentheses, not braces.
  2. data.match.payload.namesPositional patterns may bind names different from declared field names.
  3. data.match.payload.namedNamed patterns use field=pattern, and no positional pattern may follow a named one.
  4. data.match.payload.constrainLiterals and nested variant patterns may constrain payload values.

See also: Control Flow, which defines pattern scope and exhaustiveness.

#Option And Result

Option[T] and Result[T, E] are prelude enums:

Option[T] Result[T, E]
Declaration enum Option[T]: Some(value: T); None enum Result[T, E]: Ok(value: T); Err(error: E)
Values and patterns .Some(value), .None, Option.Some(value), Option.None .Ok(value), .Err(error), Result.Ok(value), Result.Err(error)
Language support beyond an ordinary enum the T? spelling; the one-layer implicit wrap of a plain T value where T? is expected; postfix ? postfix ?; the Result[void, E] entry-point result
  1. data.prelude.optionOptional T? is sugar for the prelude enum enum Option[T]: Some(value: T); None.
  2. data.prelude.resultResult[T, E] is likewise the prelude enum enum Result[T, E]: Ok(value: T); Err(error: E).
  3. data.prelude.valuesValues of either type are built with the spellings in the table, and matched with the same spellings as patterns.
  4. data.prelude.supportThe only language support beyond an ordinary enum is what the table lists for each type.
  5. data.prelude.abiThe representation of either type is an ABI decision, not a source-language difference.
  6. data.prelude.nullAn implementation may, for example, represent .None as a null reference.
fn head(items: List[i32]) -> i32?:
    if items.len() == 0: .None
    else: .Some(items[0])

See also: Optional Types, Result Types.

#Representation And Garbage Collection

  1. data.repr.implementationStruct and enum representation is chosen by the implementation subject to observable language semantics.
  2. data.repr.no-dependencePrograms must not depend on field offsets, variant tags, object addresses, or representation identity unless a future interop facility exposes them explicitly.
  3. data.repr.gcReachable composite values are garbage collected, and unreachable reference cycles are reclaimable.
  4. data.repr.no-manualThe language does not expose manual deallocation, user-visible finalizers, or weak references.
  5. data.repr.runtime-onlyWeak references and finalizers may exist only inside the standard runtime, and user code cannot observe when one clears or runs.
  6. data.repr.cleanupResource cleanup is separate from memory reclamation.
  7. data.repr.deferBlock-scoped defer provides explicit synchronous cleanup on ordinary control-flow exits and cancellation; it is not an ownership or garbage-collection mechanism.
fn work() -> void: pass

fn done() -> void: pass

fn demo() -> void:
    defer: done()
    work()

Ownership, alias-escape prevention, automatic finalization, and asynchronous or fallible cleanup policy remain deferred in the open-issues umbrella.

#Typed Derivation Of Data And Enums

Structs and enums derive library traits with @derive or a derivation block, as Typed Derivation defines.

  1. data.derive.membersFor typed derivation, a struct's members are its fields in declaration order, embedded fields included. An enum's members are each variant's payload parameters.
  2. data.derive.payload-namesAn unnamed payload parameter is the member _0, _1, and so on, by position.
  3. data.derive.sharedShared constructor data is not a member. A derivation reads it from the variant's information, and build never reads it.
  4. data.derive.newtypeA newtype derives through its base type, as Derived Newtypes defines.
@derive(Eq, Debug)
struct Point:
    x: i32
    y: i32

fn same(a: Point, b: Point) -> bool:
    a == b

See also: Members And Variants.

#Unsupported Aggregate Extensions

  1. data.unsupported.layoutStable object layout and component-model representation are ABI concerns and are not observable core-language semantics.
  2. data.unsupported.non-exhaustiveEnums have no non-exhaustive form, so adding a variant to a public enum is a breaking change for its users.

Note. A library that needs to grow a set of error kinds can wrap a private enum in a struct with a private field. It then exposes accessor methods.

@derive(Eq)
enum Kind:
    Timeout
    Refused

struct ApiError:
    kind: Kind

impl ApiError:
    fn is_timeout(self) -> bool:
        self.kind == Kind.Timeout

See also: Variant Payloads, where data.enum.payload.no-field-blocks rules out variant field blocks.

  1. 39 reads as "mi-ku" in Japanese (san-kyuu, 3-9), a nod to Hatsune Miku. The bound was not chosen for this reason, but it is a happy coincidence. ↩︎