← Back to AILP Home

fixed struct fields

A struct field declared with the fixed qualifier is initialised exactly once — at struct-literal construction time — and is rejected by the compiler at every assignment site thereafter, including through $m mutable borrows.

fixed-on-struct-field landed in v0.31.9.3 (ARIA-056, PHASE3-DEC-007/008/009). Before that slice, fixed was binding-only; struct fields silently ignored the qualifier.

Declaring a fixed field

struct:Counter {
    fixed int32:id;
    int32:count;
};

func:main = int32() {
    Counter:c = Counter{ id: 42, count: 0 };
    c.count = c.count + 1;     // OK — count is not fixed
    exit c.count;
};

The id field is initialised by the struct literal Counter{ id: 42, ... } and may be read freely. Any subsequent write fails:

c.id = 99;                     // error[ARIA-056]: cannot assign
                               //   to fixed field `id` of struct
                               //   `Counter`

Mixed-mutability semantics

A struct with both fixed and ordinary fields is fully usable through a $m borrow — the borrow does not "downgrade" the struct as a whole. Only the fixed fields are rejected on assignment; ordinary fields write through the mutable borrow as usual.

func:bump = NIL($m Counter:c) {
    c.count = c.count + 1;     // OK
    // c.id = 99;              // would be ARIA-056
    pass NIL;
};

The reverse — a $i immutable borrow over the whole struct — still rejects writes to any field (ordinary or fixed), as before.

Construction is the only write

fixed does not mean "compile-time constant" — the initial value can be any runtime expression, as long as it appears in the struct literal:

func:main = int32() {
    int32:seed = read_seed();
    Counter:c = Counter{ id: seed, count: 0 };
    exit c.id;
};

The field is bound at the moment the literal is evaluated. After that, the binding is treated as fixed for the lifetime of the struct instance.

Interaction with ..base struct update

Struct-update syntax Counter{ ..base, count: 5 } constructs a new instance; it is not an assignment to base. The new instance gets its id from base.id, but it is being initialised, not reassigned, so the rule allows it:

func:main = int32() {
    Counter:original = Counter{ id: 42, count: 0 };
    Counter:next = Counter{ ..original, count: 7 };   // OK
    exit next.id;                                     // 42
};

A field-list that omits a fixed field but explicitly names it in the update list is rejected — you cannot use ..base as a workaround to write a new value into a fixed field of the same instance.

Diagnostic

error[ARIA-056]: cannot assign to fixed field `<field>` of struct `<Struct>`
  --> source.npk:LINE:COL
   |
LL |     c.id = 99;
   |     ^^^^ fixed at struct-literal construction
   = help: fixed fields can only be initialised inside the
           struct literal; remove `fixed` from the field
           declaration to allow reassignment.

Regressions

Cross-references