Zax type definitions
| Field | Value |
|---|---|
| Status | Current conceptual design |
| Audience | Human developers defining, constructing, composing, or inspecting Zax types |
| Applies To | Ordinary named and anonymous type definitions and their programmer-visible organization; not a formal grammar or specification |
| Implementation State | Not established by this repository |
| Owns | The ordinary type-definition mental model; named and anonymous identity; direct stored shape; type-body storage, behavior, and metadata categories; incomplete self-names, forwards, recursion, completion, and outer tracked integration; explicit member layout with align, reorder, reserve, and void |
| Does Not Own | General declaration syntax (declarations and bindings); complete lifecycle behavior (construction, replacement, and destruction); qualifiers (qualifiers); composition (Zax composition); structural compatibility (structural shapes and compatibility); unions; or variants |
| Source / Provenance | Reviewed legacy type-definition evidence reconciled with current declaration, lifecycle, lifetime, qualifier, composition, structural, callable, union, and variant design |
Start with one identity and one stored shape
MyPoint :: type {
x : I32
y : I32
translate final : ()(
dx : I32,
dy : I32
) writable = {
_.x += dx
_.y += dy
}
}
myPoint : MyPoint
myPoint.translate(3, 4)
MyPoint :: type { ... } introduces one named type identity. Each MyPoint
instance directly stores x followed by y. The translate function belongs
to the type but does not add a callable field to each point: final fixes one
implementation.
Inside an instance function, _ is the
current instance. Direct member access remains
explicit:
_.x = 10
x = 10 // error unless an ordinary lexical `x` is declared
This yields the central model:
A type definition owns one identity and a declaration body. Only declarations that actually store per-instance values contribute to its direct stored shape.
What a type body can contribute
Direct stored members
An ordinary value declaration directly in the body creates one place in every instance:
MyRecord :: type {
id : U64
label : String
}
Declaration order is observable. It controls direct shape, automatic construction order, and reverse automatic destruction order.
An inferred member remains stored:
MySize :: type {
width := 0
height := 0
}
Initializers and defaults affect construction. They do not become additional shape.
Fixed and varying callable declarations
A final callable has one fixed implementation and no replaceable per-instance
slot:
MyCounter :: type {
value : Integer
increment final : ()() writable = {
++_.value
}
}
A directly declared varying callable does have per-instance callable storage:
MyHandler :: type {
callback : ()() bound
}
Its slot capacity, installed target, receiver relationship, reset, and transfer behavior are defined by lambdas and callable composition.
An unbound function may belong to the type without receiving an instance:
MyMath :: type {
twice final : (
result : Integer
)(
value : Integer
) unbound = {
return value * 2
}
}
A once declaration belongs to the complete type rather than adding ordinary
storage to every instance. This includes callable slots and any value category
for which once storage is available. Exact global/once value initialization,
concurrency, and teardown remain future lifecycle work.
Receiverless and instance-qualified once calls retain the Nothing-instance
behavior defined by
Zax Nothing instances.
Definitions, routes, and metadata
Nested types, transparent aliases, reshapes, abstract roles, fulfillments, data routes, preferred routes, exposure metadata, and fixed operators do not become direct stored members merely because they appear in the body:
MyContainer :: type {
MyIndex :: alias type U32
payload : MyPayload
label via payload.label
inspect final : ()() readonly = {
print(_.label)
}
}
Each declaration category follows its own owner. Future reflection must keep a physical stored member distinct from an alternate path, generated wrapper, fixed implementation, type-owned slot, or metadata-only declaration.
Named identity is distinct from equal shape
Two separately named types remain distinct even when their stored members look the same:
PixelPoint :: type {
x : I32
y : I32
}
KilometerPoint :: type {
x : I32
y : I32
}
pixel : PixelPoint
kilometers : KilometerPoint
pixel = kilometers // error: distinct identities
An explicit compatible construction or transformation can cross an accepted structural relationship. Matching storage never makes the identities interchangeable by default. Complete behavior is defined by structural shapes and compatibility.
A transparent alias adds another name for one identity:
ScreenPoint :: alias type PixelPoint
No conversion is required between ScreenPoint and PixelPoint.
Anonymous type definitions
An ordinary value can define an anonymous type at its declaration:
myPair : :: type {
left : Integer
right : Integer
}
This declaration creates one stable compile-time identity. Inferred copies reuse that identity:
another := myPair
MyPairType :: alias type type of myPair
named : MyPairType = myPair
Equal anonymous source written at another declaration does not by itself create the same identity. Current design establishes no anonymous recursive type syntax.
The specialized anonymous forms follow the same identity rule:
bits : :: union {
byte : U8
word : U32
}
choice : :: variant {
text : String
number : U32
}
Their storage and lifecycle behavior belongs to unions and variants. Category-specific aliases preserve those identities:
MyBits :: alias union type of bits
MyChoice :: alias variant type of choice
Self-reference, recursion, and completion
A named type’s own incomplete name is visible while its body resolves:
MyNode :: type {
value : Integer
next : MyNode * unique
}
The pointer representation is finite. Its default state is vacant, and its ownership contract governs any later pointee.
Direct recursive storage has no finite layout:
MyBrokenNode :: type {
next : MyBrokenNode // error: infinitely recursive stored layout
}
The same rule applies through several types. Use a finite indirection wherever a recursive cycle would otherwise require inline storage.
The self-name supplies identity before completion, but facts requiring the final member set remain pending:
- size and alignment;
- direct and flattened shape;
- generated lifecycle operations;
- constructor viability;
- union admissibility;
- variant payload extent; and
- reflection of the completed declaration.
Forward anchors
forward type establishes a source-ordered name and category before the
definition appears:
MyOtherNode :: forward type
MyNode :: type {
next : MyOtherNode *
}
MyOtherNode :: type {
next : MyNode *
}
The forward supplies no body, layout, members, or initialization behavior. It
must receive exactly one matching completion. A type does not need forward
merely to use its own self-name.
Complete path and forward behavior belongs to declarations and bindings.
Specialized definitions preserve their own forward categories:
MyOverlay :: forward union
MyChoice :: forward variant
Completion must be a matching direct declaration or an exact alias to an existing identity of that declaration category.
Construction and member lifetime
For ordinary direct members:
- automatic construction follows declaration order;
- explicit constructor-body member operations may choose another tracked control-flow order;
- a constructor must normally complete every required member;
- the enclosing destructor body runs while remaining members are live; and
- automatic member destruction follows reverse declaration order.
MySession :: type {
connection : MyConnection
log : MyLog
+++ final : ()(
endpoint : MyEndpoint
) = {
_.connection.+++(endpoint)
// log uses its ordinary automatic construction.
}
}
Complete replacement renews every member resident instance even when a custom replacement constructor retains a resource or leaves bytes at the same address. See construction, replacement, and destruction and lifetimes and references.
Qualifications follow the actual path
The containing value, each direct member, and every pointer, reference, or wrapper layer retain their resolved qualifications:
record : MyRecord
reader : MyRecord readonly & = record
record.label = "changed"
reader.label = "blocked" // error: member path is readonly
Deep immutability follows direct structural containment. It does not silently rewrite pointee, referent, optional payload, or variant payload qualifications. Complete behavior belongs to Zax qualifiers.
Composition preserves physical containment
MyEngine :: type {
rpm : Integer
start final : ()() = {
}
}
MyCar :: type {
engine own expose : MyEngine
wheels : Integer
}
myCar : MyCar = [{
.engine = makeMyEngine()
}]
myCar.rpm = 2000 // published path to engine.rpm
myCar.start() // exposed forwarding behavior
myCar.rpm and myCar.engine.rpm reach the same stored member place.
myCar.start() is generated behavior. Neither relationship flattens physical
construction or destruction: .engine remains the direct construction-packet
entry, and MyEngine remains one complete contained value.
Complete publication, projection, forwarding, and role behavior belongs to Zax composition.
outer tracked is an explicit type capability
MyTrackedEngine :: type outer tracked {
rpm : Integer
}
MyTrackedCar :: type {
engine : MyTrackedEngine
}
recoverMyCar final : (
result : MyTrackedCar & ?
)(
engine : MyTrackedEngine &
) = {
return engine tracked outer cast MyTrackedCar.engine
}
outer tracked permits hidden representation and lifecycle bookkeeping that
describes the current resident instance’s immediate placement. A standalone
engine and an engine stored at MyTrackedCar.engine have different
relationships.
Construction, copy, move, and replacement establish destination-correct tracking. Raw bit relocation is not sufficient. The capability changes shape, cost, and compatibility even though its hidden component is not an ordinary source-declared member.
Complete outer-cast behavior and costs belong to Zax composition.
Unions and variants are specialized type definitions
An unmanaged union declares several typed lenses over one untagged backing representation. Lenses are not ordinary resident members and have no individual construction or destruction.
A managed variant declares several named alternatives while allowing zero or one payload life path to be active. Its wrapper owns selection, construction, destruction, and replacement.
They intentionally do not share one “active member” model.
Explicit member layout
Usually the compiler decides how members are aligned and how much padding sits between them, while keeping members in declaration order. When a layout must match something outside the program, such as a network packet, a file format, or a hardware register block, directives state it explicitly:
MyPacketHeader :: type {
size : U32
[<align=16>]
id : Uuid // placed at an offset that is a multiple of 16
[<reserve=100>] // 100 unnamed bytes at this point
[<void>]
checksumView : U64 // has an offset here, but occupies no bytes
checksum : U32 // shares checksumView's offset
}
Alignment
[<align=n>] on the line above a member places that member at an offset that is
a multiple of n. n must be a power of two; any other value is an error.
Member order
Members are never reordered unless the type asks. [<reorder=compact>] or
[<reorder=speed>] above a type definition lets the compiler choose an order
that minimizes size or favors access speed; reorder=none is the default. A
reorder changes only where members are placed. Construction and destruction
still follow declaration order.
[<reorder=compact>]
MyStats :: type {
enabled : Boolean
total : U64
flags : Byte
}
Reserved bytes
An unattached [<reserve=n>] inside a type body adds n unnamed bytes at that
point. Reserved bytes have no initial value and carry no value, like the padding
described for unions: copying, equality,
and construction never depend on their contents.
Members that occupy no space
[<void>] above a member gives that member an offset but no size. The next
member starts at the same offset. Reading a void member reinterprets whatever
bytes are at its offset, including later members and possibly bytes beyond the
type’s own storage, such as variable-length data that follows a header in
memory.
That reinterpretation is unchecked. Writing [<void>] is itself the visible
promise that the access is valid, as with other
unchecked-promise directives,
so no separate unsafe marker is required.
Layouts that differ by environment
When a layout must differ between targets, select it while compiling and mark
the selected member outward:
MyBlock :: type {
if compile size of Target.Integer > 4 {
[<align=16>]
data outward : MyPayload
} else {
data outward : MyPayload
}
}
Layout and partials
By default, partials cannot
add stored members, so an explicit layout stays as written. A type that writes
seal open storage lets partials append storage after its own members.
reserve and void describe positions among the type’s own members, so they
keep their meaning, but the total size grows. A layout whose total size must
stay fixed simply leaves storage closed.
Partial and generic completion boundaries
Another declaration can add to a type through a
partial. A type states what others may add with a seal
clause:
MyCatalog :: type seal open storage {
entries : Integer
}
By default, others may add functions, nested types, and once state, but not
stored members. seal open storage allows added stored members, and bare
seal allows nothing.
Added stored members change the type’s size, layout, and generated lifecycle,
so those facts wait until every storage addition is known. For a type that
opens storage, that happens when its own module is finalized; importers see
the final layout as soon as the import completes. A type with
seal open storage module waits until the whole application is assembled.
Until then, size of and the other completion facts listed under
self-reference, recursion, and completion
remain pending.
A partial never adds union lenses or variant alternatives. Complete partial behavior, including which categories exist and how storage is included, belongs to Zax partials.
Likewise, a generic instantiation must become one complete concrete type before size, recursive layout, generated lifecycle operations, union admissibility, or variant storage and coverage are decided. Exact generic syntax remains future work.
Costs and diagnostics
Programmers must be able to discover:
- direct stored size, alignment, padding, and hidden structural components;
- automatic initialization and destruction of every direct member;
- allocation, reference counting, or user code performed by member lifecycle;
- per-instance varying callable storage versus fixed implementations;
- type-owned
oncestate; - composition wrappers and tracking costs; and
- source-compatibility effects from member, qualification, or operation-family changes.
Diagnostics should distinguish:
- identity mismatch from structural incompatibility;
- infinite recursive storage from valid finite indirection;
- incomplete identity use from a request requiring completed layout;
- missing or duplicate forward completion;
- stored member declarations from no-storage definitions;
- implicit instance-member lookup without
_.; - unavailable generated lifecycle caused by one member path; and
- hidden
outer trackedrepresentation from ordinary visible data.
Source stability and maturity
Changing a stored member’s name, order, type, qualification, or lifecycle can change layout, construction, destruction, compatibility, reflection, and generated operations. Adding a fixed function does not change stored shape; adding varying per-instance callable storage does.
This document defines current conceptual design, not formal grammar, ABI, reflection schema, compiler lowering, or a conformance specification.