Zax conversions and casts
| Field | Value |
|---|---|
| Status | Current conceptual design |
| Audience | Human developers converting values between types or reinterpreting memory |
| Applies To | Choosing among conversion, stance restatement, structural compatibility, container and member navigation, and unchecked reinterpretation; programmer-declared as; unsafe cast; pointer and integer conversion; not a formal grammar or specification |
| Implementation State | Not established by this repository |
| Owns | The overview of conversion and cast operations; programmer-declared as conversion; unsafe cast forms, vacancy, qualification, ownership-role, cost, and diagnostic behavior; conversion between pointers and pointer-representation integers |
| Does Not Own | Numeric conversion (integers, fixed-point, floating-point); admission and projection (identity types, enums); stance restatement (transfer stances); compatibility, coercion, and structural applications of unsafe cast (structural shapes); outer casting (composition); interior ownership (pointers); dereference (Nothing instances); exact forms (operator catalog) |
| Source / Provenance | Legacy casting material from the retired root casting page, refined against current operator, structural, pointer, qualifier, and scalar design |
| Supersedes | The retired root casting page |
Zax has no universal cast
Zax offers several operations where many languages offer one cast, because speed, safety, and efficiency matter more here than one flexible spelling. You choose the operation that matches your intent, from a free reinterpretation of existing memory to a conversion that builds a new value. Each of these lines asks a different question:
display := boiling as Fahrenheit // Celsius chose to provide this conversion
myByte := myCount narrowing as U8 // the language's modular integer conversion
header : PacketHeader * =
myBuffer unsafe cast PacketHeader * // treat these bytes as a header; nothing is checked
The first line runs code that the Celsius type wrote. The second follows the
integer rules for keeping only the low bits. The third converts nothing at all:
it tells the compiler to treat existing memory as a different type, and the
programmer takes responsibility for that being valid.
Keeping these operations apart lets a reader see the cost and the risk of each
line. A conversion can run code. Restating a stance moves nothing by itself. An
unchecked reinterpretation carries unsafe in its spelling.
as is an ordinary operator word. Most as operations are operators that a
type chooses to declare. The language owns only a small set of exact as ...
forms, such as stance restatement and structural compatibility, plus the
language-provided conversions of its own scalar types.
Which operation do I want?
| I want to… | Representative source | Taught by |
|---|---|---|
| Convert a number to another numeric type | myWide := myU16 as U32, myMaybe := myU16 as U8? |
integers, fixed-point, floating-point |
| Convert my type to another type | display := boiling as Fahrenheit |
Writing your own as below |
| Wrap a value in an identity or enum type | myCount := MyCount from myRaw |
identity types, enums |
| Hand a value to a consumer as a move | consume(myBuffer as move) |
transfer stances |
| Use compatible storage as another named type | pixel : PixelPoint = kilometers as shape PixelPoint |
structural shapes |
| Get the container from a member | car : Car & = engine outer cast Car.engine |
composition |
| Own a member of a shared allocation | engine : Engine * strong = inner car.engine |
pointers |
| Take an address, or follow a pointer | pointer : MyValue * = value, view : MyValue & = pointer. |
declarations, Nothing instances |
| Store an address as an integer | myAddress : UPointer = myPointer as UPointer |
Pointers and integers below |
| Reinterpret memory without checks | header : PacketHeader * = myBuffer unsafe cast PacketHeader * |
Unchecked reinterpretation below |
Taking an address needs no cast. A pointer initialized from a value takes that
value’s address, and a trailing . follows a pointer to its target. Both rules
are explained where they are owned.
Writing your own as
A type provides a conversion by declaring binary as with a type parameter
slot:
Fahrenheit :: type {
degrees : Binary64
}
Celsius :: type {
degrees : Binary64
operator binary 'as' final : (
result : DestinationType
)(
DestinationType : type
) readonly = {
result .= [{
.degrees = degrees * 9.0 / 5.0 + 32.0
}]
}
}
boiling : Celsius = [{
.degrees = 100.0
}]
display := boiling as Fahrenheit
The left value’s type owns the conversion, so boiling as Fahrenheit finds the
operator on Celsius. DestinationType is a
type argument:
it names a type and has no runtime storage, lifetime, or evaluation.
The result starts unconstructed. result .= [{ ... }] constructs it from the
packet, just as the same packet would initialize a declaration. Writing
result.degrees = ... first would be an error, because ordinary = never
constructs. See
constructing a result later.
Write each declaration for one intended destination type. A type parameter slot
cannot yet restrict which types a caller may name, so the declaration above
would also be selected for some unrelated DestinationType. It would then fail
to compile if that type has no degrees member. Restricting accepted type
arguments is future generic work.
A declaration may name a pointer or reference type as its destination. The
result then follows the ordinary rules for
returned references. There
is no built-in as MyType *; initializing a pointer from a value already takes
its address.
A type may also declare longer phrases that begin with as, such as
as json text. They work, but a lint discourages them: the language may add
new exact as ... forms later, and a collision would change what existing
source means. Plain binary as with a type argument is always safe to declare
on your own types.
Words inside a phrase are phrase words, not keywords. A type may name an
operation unsafe as to warn its readers, but that word grants no unsafe
authority. The language’s own unchecked reinterpretation is unsafe cast.
Your types and the language’s scalars
Integers, fixed-point, and floating-point conversions belong to the language. Your type can still convert to and from them:
myCount := myTally as I32 // MyTally declares its own `as`
myTally : MyTally = [{
.count = myCount // build a MyTally by construction
}]
myId : UserId = UserId from myRaw // an identity type admits its underlying value
A scalar value is the receiver of myCount as MyTally, so MyTally cannot
supply that conversion. Use construction instead, or an identity type’s
admission.
A partial does not solve this. A partial can add operators to
I32, but an as declaration accepts every destination type, so a partial
as on I32 would also claim myCount as I64, which belongs to the language.
It becomes a practical route only once a declaration can restrict its
destination type.
Unchecked reinterpretation with unsafe cast
unsafe cast tells the compiler to treat existing memory as another type
without converting or checking anything. The result is always a view, a pointer
or a reference, never a new value:
otherPointer := myPointer unsafe cast OtherType * // pointer to pointer
valueView := myValue unsafe cast OtherType & // a value's storage viewed as OtherType
referenceView := myReference unsafe cast OtherType & // reference to reference
The cast verifies no compatibility, qualification, invariant, lifetime, bounds,
alignment, or layout guarantee. Those facts become the programmer’s
responsibility, and a false claim has undefined consequences. The compiler
still rejects a reinterpretation it proves impossible, even when unsafe is
written.
Before reaching for unsafe cast, check whether a checked operation expresses
the same intent. The
structural compatibility and coercion forms
prove layout and validity facts for related types, and a declared as performs
real conversion.
New values come from construction
A cast produces no new value, so there is no by-value form. To obtain a new value, form the view and construct from it:
copy := myValue unsafe cast OtherType // error: unsafe cast produces a view
copy : OtherType =
myValue unsafe cast OtherType & // view the storage, then construct a copy
A by-value spelling could not say whether it meant a byte copy, a constructor call, truncation, or the start of a new lifetime. Separating the view from construction keeps each responsibility visible: the cast owns the unchecked reinterpretation, and construction owns copying, resources, and the new lifetime.
Vacant pointers stay vacant
A vacant pointer converts to a vacant pointer of the destination type:
vacantPointer : MyType *
otherPointer := vacantPointer unsafe cast OtherType *
// ?otherPointer is false: still vacant
Each type may represent vacancy differently, because a vacant pointer targets
its own type’s Nothing instance. Without this rule, the
result would point at MyType’s Nothing backing while claiming to be an
OtherType pointer, and a presence test would report it as present. A
non-vacant address is reinterpreted unchanged.
To keep a vacant pointer’s exact raw representation instead, convert it to a pointer-representation integer and back. That round trip does not preserve vacancy.
Qualification
The written destination type states the complete qualification of the result.
unsafe cast can therefore remove immutable, readonly, or final, or add
promises that no ordinary conversion allows:
myMutable : MyType = makeValue()
asReadonly : MyType readonly & = myMutable // ordinary: readonly narrows one path
asImmutable : MyType immutable & = myMutable // error: another path may still change it
trusted := myMutable unsafe cast MyType immutable & // programmer asserts nothing will change it
immutable promises that the value never changes for its whole lifetime. A
mutable value cannot make that promise through an ordinary view, so adding it
needs unsafe cast.
When the only goal is to change qualification on the same type, prefer
unsafe pliable. It keeps the recorded
qualifications and bypasses them locally for one explicit path, while
unsafe cast creates a differently qualified view that later code cannot
distinguish from an ordinary one.
Pointer ownership roles
For raw, unique, unique shareable, strong, and weak pointers,
unsafe cast changes only the pointee type. The change of ownership role, the
transfer stance, and every lifetime rule are exactly those of the ordinary
pointer conversions:
myCar : Car * strong = makeCar()
shared : Truck * strong = myCar unsafe cast Truck * strong // another strong owner
observer : Truck * weak = myCar unsafe cast Truck * weak // ordinary strong to weak
exclusive : Truck * unique = myCar as last unsafe cast Truck * unique // ordinary unique-claim rules
A pointer with a control block (strong, weak, or unique shareable) still
disposes the allocation its control block records. Viewing a Car allocation
as a Truck does not make release destroy a Truck.
A blockless unique pointer may also change its pointee type. Whether that is
valid depends on the program; the compiler cannot tell, and the unsafe
spelling says so. Which type’s destruction runs when a retyped blockless
unique releases is not yet specified. Code that retypes one takes
responsibility for correct disposal.
Pointers and integers
An address can be stored as an integer and turned back into a pointer:
myAddress : UPointer = myPointer as UPointer // observe the address
myRestored : MyType * = myAddress unsafe cast MyType * // assert it is a valid MyType address
Converting a pointer to an integer is safe. Observing an address cannot break memory safety. The integer carries no provenance, though, so it proves nothing if it is later turned back into a pointer.
Converting an integer to a pointer is unchecked reinterpretation. The programmer asserts that the address is valid for the destination type, suitably aligned, and live for every use.
- The integer must match the pointer’s memory domain:
UPointerfor ordinary pointers,Near.UPointerfor near pointers, andFar.UPointerfor far pointers. See pointer-representation integer roles. - A vacant pointer converts to its raw representation. That value is not portable, and converting it back produces a pointer that is not vacant.
- The difference between two addresses uses
PointerDelta, the exact difference type ofUPointer.
Costs
- A declared
ascosts whatever its body does, including allocation or other resource work. unsafe castperforms no conversion work. Preserving vacancy may need one test of the source pointer. That test disappears when the source and destination use the same vacant representation, or when the compiler proves that the source is never vacant.myPointer as UPointerand integer-to-pointerunsafe castcopy an address.- Every
unsafeform adds audit cost: a reviewer must confirm the facts the compiler did not check, and the result may be less portable across targets.
Diagnostics
Useful diagnostics include:
- by-value
unsafe cast, with the view-then-construct correction; - a reinterpretation the compiler proves impossible, even under
unsafe; - an ordinary view that tries to add
immutableor remove a qualification, withunsafe pliableandunsafe castshown as the explicit alternatives; - a pointer-to-integer conversion whose integer role does not match the pointer’s memory domain;
- a declared
asselected for a destination its body cannot produce; and - a lint for programmer-declared phrases that begin with
as.
Boundaries and maturity
These concerns remain future work:
- restricting which destination types one
asdeclaration accepts; - adding operators to the language’s scalar types from programmer code;
- helpers that view raw memory as an array or iterator of bytes;
- pointer provenance, alignment, segment, and comparability rules beyond the conversions described here; and
- which type a retyped blockless
uniquedestroys.