Zax variants
| Field | Value |
|---|---|
| Status | Current conceptual design |
| Audience | Human developers representing one named managed alternative, or absence, in one value |
| Applies To | Programmer-visible variant declaration, construction, selection, access, transfer, qualification, and lifetime behavior; not a formal grammar or representation specification |
| Implementation State | Not established by this repository |
| Owns | Variant wrappers and named alternatives; absent/present state; direct packets; protected contained reconstruction; reset; checked named access; wrapper and payload lifecycle; whole-wrapper transfer; body organization; variant costs, diagnostics, and source stability |
| Does Not Own | Exact switch clause mechanics (switch, case, and default); general construction machinery (construction, replacement, and destruction); general transfer meaning (transfer stances); general qualifiers (qualifiers); generalized pattern matching; or ABI/layout contracts |
| Source / Provenance | Maintainer-reviewed managed-alternative design reconciled with optional, lifetime, construction, transfer, qualifier, composition, switch, source, and safety owners |
Start with absence, then select one named payload
MyMessage :: variant {
number : U32
alternateNumber : U32
text : String
alternateText : String
owner : MyPayload * unique
}
message : MyMessage
if !message
print("no active alternative")
message.text .= "ready"
print(message.text)
incoming := receiveMyMessage()
switch incoming {
case text {
// Variant routing binds the active payload as local `text` access.
print(text)
}
case ? bind other
handleOtherPresent(other)
case ! {
print("absent")
}
}
A variant has one live wrapper and zero or one active named payload:
- default construction creates absence;
.=can select and construct one alternative;- exactly one payload life path exists while present;
- changing selection ends the old payload and begins another; and
- wrapper destruction destroys the active payload when present.
Alternative names are part of the state. text and alternateText remain
different alternatives even though both store String.
A variant is a managed wrapper around absence or one named payload. It is not an untagged view of overlapping bits.
Use an unmanaged union when several typed lenses should interpret the same passive representation without selection or payload lifecycle.
Declaring alternatives and behavior
A variant body begins with its alternative prologue:
In variant switch mode, one alternative name both routes selection and
introduces clause-local reference-shaped access under that name. The complete
binding and coverage model is taught later under
variant switch.
MyResult :: variant {
value : MyValue
error : MyError
describe final : ()() readonly = {
switch _ {
case value
print(value)
case error
print(error)
case !
print("absent")
}
}
}
Every per-instance stored declaration is an alternative. The first declaration that adds no per-instance storage begins ordinary definition mode; alternatives cannot resume afterward.
Allowed later declarations include:
finalfunctions and operators;oncedeclarations whose state belongs to the complete type rather than every variant instance;- aliases and nested types; and
- other genuinely no-instance-storage definitions.
A final value is still storage and therefore still an alternative. A direct
varying callable declaration would create hidden per-instance storage outside
selection and is rejected:
MyInvalidVariant :: variant {
value : MyValue
replaceable : ()() bound = { // error: hidden varying callable slot
}
}
Store the callable explicitly as an alternative when needed:
MyCallbackChoice :: variant {
callback : ()() bound strong
disabled : MyDisabledMarker
}
Named, forwarded, and anonymous variants
A named variant exposes its incomplete self-name while its body resolves. Direct self payload storage is infinitely recursive; finite indirection is valid.
Source-order or mutual relationships use:
MyChoice :: forward variant
The anchor must complete through a direct variant declaration or an exact alias to an existing variant:
MyChoice :: alias variant ExistingVariant
An anonymous variant creates one stable compile-time identity:
choice : :: variant {
text : String
number : U32
}
MyChoiceType :: alias variant type of choice
Equal anonymous source elsewhere does not imply the same identity. Anonymous recursive self-syntax is unavailable.
An exact alias preserves the specialized category:
MyChoiceAlias :: alias variant MyChoice
Default and direct construction
Default construction is absent:
first : MyMessage
second : MyMessage = [{}]
Both wrappers are live and absent. The explicit zero-entry packet invokes the same zero-input/default construction.
A variant construction packet names at most one alternative:
present : MyMessage = [{
.text = "ready"
}]
The packet directly constructs text. It does not first construct absence and
assign over it.
Several alternative entries are invalid:
invalid : MyMessage = [{
.number = 10,
.text = "ten" // error: one variant cannot construct two alternatives
}]
Nested construction remains explicit:
MyConnectionChoice :: variant {
connection : MyConnection
unavailable : MyUnavailable
}
choice : MyConnectionChoice = [{
.connection = (: MyConnection = [{
endpoint,
mode: selectedMode
}])
}]
The inner packet constructs MyConnection; the outer packet selects the
connection alternative.
Direct construction from another equal variant type preserves its absent or named-present state under the selected transfer stance.
.= reconstructs contained state
.= is the protected, non-overloadable contained-reconstruction operator
shared with optional values.
Named-alternative target
message.text .= "first"
message.text .= [{ "second" }]
The left side is a destination designator, not a read of the old payload. Either form:
- evaluates and binds its source inputs;
- destroys the old active payload when present;
- constructs the named payload;
- publishes that active name only after construction completes; and
- returns access to the newly constructed payload.
Selecting the already active name still renews its payload lifetime.
Wrapper-packet target
An existing wrapper can receive a complete variant packet:
message .= [{}] // reconstruct absent state
message .= [{
.number = 42
}]
The packet contains zero alternative entries for absence or exactly one for presence. This form returns access to the wrapper because its zero-entry case has no payload.
A bare value cannot choose an alternative:
message .= 42 // error: name the alternative
message.number .= 42
Payload type alone is insufficient because several alternatives may have the same type.
Wrapper, contained reconstruction, and ordinary assignment differ
message = otherMessage // assign state within this wrapper lifetime
message .= otherMessage // reconstruct the complete wrapper lifetime
message.text .= "new" // end old payload and construct text
message.text = "updated" // checked access, then ordinary String assignment
The punctuation makes the lifetime choice visible:
- same-type wrapper
=assigns state within a mutable wrapper lifetime; - same-type wrapper
.=reconstructs the complete wrapper lifetime; - named-alternative
.=reconstructs contained state; - named-payload
=invokes ordinary in-lifetime assignment through existing checked access; and - postfix or named access reaches an existing payload.
.= has assignment precedence and right association. Named-alternative
reconstruction requires a mutable wrapper and writable path. A final or varying
wrapper place may use that contained operation because the wrapper lifetime
continues. Same-type wrapper = has the same mutable/writable requirement.
Same-type complete-wrapper .= instead requires a varying wrapper place,
declaration-side replacement permission, and writable access.
Every source input is secured before the old payload ends. An unresolved
reference into the old payload is an alias error or uses the applicable
replacement-alias unsafe responsibility. A panic leaves the same operation
blocked and exposes neither a partial payload nor rollback.
Complete shared ordering and alias behavior belongs to construction, replacement, and destruction.
Reset returns the same wrapper
reset message
Ordinary reset is complete at that statement. Its returned access may be used when another expression needs the now-absent wrapper:
cleared : MyMessage & = reset message
reset:
- destroys the active payload once when present;
- is a no-op when already absent;
- leaves the same wrapper lifetime absent; and
- returns access to that wrapper without increasing authority.
This permits explicit reconstruction:
(reset message).text .= "replacement"
Direct payload destruction is unavailable because it would leave the wrapper’s selection state incorrect:
message.text.---() // error
Replace an already active payload value
Named-alternative .= is wrapper-owned selection and performs fresh ordinary
payload construction:
choice.replaceable .= [{ newValue }]
It does not select the old payload type’s +++ replacement. To reconstruct the
active payload value itself, first establish a case binding or explicit
reference:
switch choice {
case replaceable {
replaceable .= [{ newValue }]
// Complete payload replacement; may select payload +++ replacement.
}
}
The binding must reach a type-side varying payload place through writable, declaration-side varying access. A final payload place or readonly payload path rejects complete reconstruction. No extra trailing-dot variant syntax exists.
Presence and named access answer different questions
?message asks whether any alternative is active:
if ?message
print("some named payload is present")
It does not prove which name is active.
Direct named access uses the variant’s selection state:
textView : String readonly & = message.text
Four outcomes are distinguished:
- If
textis proved active, access succeeds and needs no runtime check. - If
textis proved inactive, source is a compile-time error. - If the active name is not proved, access checks at runtime and panics when
textis inactive. - If that registered check is disabled, the programmer promises
textis active; violation has undefined consequences.
The panic category is inactive-variant-access, enabled by default. It applies
to reads, reference formation, ordinary assignment, compound operations, and
calls through a named alternative. It does not apply to .= or a switch route
that establishes the active name.
Exact panic-control source syntax remains future analysis-control work. Static proof may always remove an enabled check, while a statically proved mismatch remains an error even when the category is disabled.
Payload references end with that payload
message.text .= "first"
oldText : String readonly & = message.text
message.text .= "second"
use(oldText) // error: the first text lifetime ended
The same invalidation occurs when:
- another alternative is selected;
- the wrapper is reset;
- the complete wrapper is replaced; or
- the wrapper is destroyed.
Address reuse does not preserve the old payload instance. A reference into an active alternative remains tied to that exact conditional life path.
Wrapper and payload qualifications are separate
The wrapper owns selection; the selected type owns payload behavior.
- wrapper
mutablepermits selection or presence to change during one wrapper lifetime; - wrapper
immutablekeeps absence or the selected name stable for that wrapper lifetime; - writable access is required to reset or use
.=; and - complete wrapper replacement additionally requires a varying place and declaration-side replacement authority.
Payload access carries the payload’s own resolved qualifications:
MyChoice :: variant {
stableText : String immutable
workingText : String mutable
}
An authorized mutable/writable wrapper may end stableText and select another
payload. That operation ends a conditional path; it does not mutate the
immutable String through payload access.
A readonly path cannot reset or select:
reader : MyChoice readonly & = choice
reader.workingText .= "blocked" // error: wrapper path is readonly
Copy, deep, move, and last
Whole-wrapper transfer preserves absence or the active name:
copycopies the active payload and leaves the source unchanged;deeprequires the active payload’s exact independent-copy contract;moveleaves the source present with one moved-from payload;lastleaves the source present with one terminal but destruction-valid payload; and- reset or ordinary wrapper destruction later ends that remaining payload.
MyOwnerChoice :: variant {
owner : MyPayload * unique
unavailable : MyUnavailable
}
owners : MyOwnerChoice
owners.owner .= makeOwner()
next : MyOwnerChoice = owners as move
// next.owner owns the transferred resource.
// owners remains present with a moved-from owner payload.
reset owners
The operation family must be viable for every alternative that the wrapper may hold at that use. When flow proves one active name, source can operate on that payload directly.
last never silently resets the source wrapper. Payload transfer and selection
state are separate. Moving a payload into another variant therefore leaves the
source active until reset or another lifecycle operation ends it.
General stance meaning and fallback belong to Zax transfer stances. A case binding reached from a by-value selector preserves that selector’s projected offered stance but performs no transfer merely by binding.
Select alternatives with variant switch
When a switch selector has variant type, case entries use a dedicated
alternative-routing mode:
switch message {
case text {
print(text)
}
case number, alternateNumber bind selectedNumber {
print(selectedNumber)
}
case ? bind other
handleOtherPresent(other)
case ! {
print("absent")
}
}
case textresolvestextinMyMessage’s alternative namespace and binds reference-shaped payload access under that same name.bindgives several alternatives one common body-local binding.- a several-name case without
bindroutes control but introduces no payload local; - the body is checked separately for every alternative that can select it;
case !selects absence and introduces no payload binding.case ? bind valueselects any remaining present alternative and specializes the body for every one.defaultremains positional fallback.
Specific names may appear before the present catch-all:
switch message {
case text
print("text:", text)
case ? bind other
print("another present payload:", other)
case !
print("absent")
}
Case bindings are aliases, not copies. They preserve origin, qualifications, and the transfer stance projected from the retained selector.
Complete ordering, labels, direct entry, continuation, coverage, and diagnostics are defined by switch, case, and default.
Coverage and partial selection
Variant selection checks two dimensions:
- Alternative coverage: every declared name is handled by a named case or
case ?. - State coverage: absence reaches
case !ordefault.
default is fallback but does not silently count as explicit alternative
coverage. Deliberately omitting alternatives uses:
intent<partial-variant-selection>{
switch message {
case text
print(text)
default
handleOtherState()
}
}
The acknowledgement preserves the fallback behavior. It does not bind an omitted payload or make the switch exhaustive under another meaning.
Adding an alternative makes a named-complete switch incomplete. A case ?
deliberately accepts future present alternatives when its body still specializes
successfully.
Control-flow entry cannot manufacture a binding
A direct goto into a payload-bound clause is illegal because it would bypass
selection and binding.
A continue case_label: may retest the target route and enter only after it
establishes the binding. next, break, and outward transfer retain their
ordinary meanings.
Bare case ?, case !, default, and transfer-only clauses introduce no
payload binding. Direct entry can target an otherwise eligible unbound body, but
the path receives no fact from the bypassed test. A body may use only facts
common to every incoming path.
Recursive and nested alternatives
Direct recursive payload storage has no finite extent:
MyRecursive :: variant {
next : MyRecursive // error: infinitely recursive payload storage
}
Finite indirection is valid:
MyRecursive :: variant {
end : MyEnd
next : MyRecursive * unique
}
An optional, pointer, nested variant, or comparable payload keeps its own semantic boundary. Variant selection never flattens nested absence, vacancy, or selection state.
Complete generic variant formation remains future generic work. Every instantiation must close to one concrete alternative set before maximum storage, lifecycle families, and coverage are checked.
Alternatives are not structural anchors
An alternative is a conditional payload path, not a statically resident member path. It cannot be a compatibility anchor:
choice anchor .text // error: conditional alternative
A physical member whose complete value is a variant can be anchored as one atomic wrapper:
container anchor .choice
That operation does not enter or select the active payload. Complete anchor behavior belongs to structural shapes and compatibility.
Composition stops at the variant wrapper
Composition modifiers on a stored variant member apply to the wrapper value. They do not enter whichever payload is active:
MyDetails :: type {
label : String
}
MyPanel :: type {
content : MyContentChoice
}
usePanel final : ()(panel : MyPanel readonly &) = {
inspect(panel.content)
inspect(panel.label) // error: no implicit delegation through the variant
}
own, preferred, expose, and outer casting cannot erase visible alternative
selection, absence, aliasing, lifetime, or runtime cost. Any future indirect
delegation facility must state those consequences explicitly.
Costs and representation
A variant requires enough representation to distinguish absence from every alternative and to store its largest payload with suitable alignment.
The language does not promise:
- a particular discriminant width or location;
- a separate tag rather than a valid niche;
- one payload offset;
- stable ABI or calling convention;
- raw relocatability; or
- one lowering strategy.
Programmer-visible costs include:
- discriminant or equivalent state;
- runtime checked named access when proof is unavailable;
- payload construction and destruction during
.=and reset; - per-alternative transfer dispatch;
- generated body specialization for polymorphic cases; and
- larger size or alignment when alternatives change.
Diagnostics
Diagnostics should distinguish:
- absent access from a different active name;
- a compile-time-proved inactive name from runtime-checked uncertainty;
- disabled
inactive-variant-accesswhose promise is known false; - a packet with several alternatives;
- bare wrapper
.=without a named alternative packet; - ordinary
=used where contained reconstruction.=is intended; - wrapper replacement used where payload assignment is intended;
- aliasing into a payload that
.=will end; - reference use after reset or reselection;
- missing payload operation blocking whole-wrapper transfer;
- incomplete alternative or absence coverage;
- a direct control-flow entry that cannot establish a binding;
- a polymorphic case body failing for one named alternative;
- hidden varying storage declared after the alternative prologue;
- a custom lifecycle declaration that conflicts with protected wrapper behavior; and
- direct recursive payload storage.
Source stability and maturity
Adding, removing, renaming, or retyping an alternative can change size,
alignment, transfer availability, switch coverage, specialization, and source
validity. Adding an alternative is intentionally visible to named-complete
switches; case ? is the explicit future-present catch-all.
This document defines current conceptual design, not formal grammar, a generalized pattern system, ABI, reflection schema, discriminant layout, concurrency protocol, or compiler implementation.