Value representations
Classes, tuples, and enum payloads keep fields inline in their owner’s storage. Native slots contain ordinary value bits plus any required inline payload bytes. Moves and copies relocate private addresses into the destination; addresses never extend the lifetime of departed frames. Immutable metadata describes field layout, ownership, relocation, equality, and formatting. Concrete layout facts are cached.
Runtime record traversal advances a running slot offset, including each field’s inline payload and any class cleanup state word. A complete pass takes one width lookup per field, without allocating an offset table. Hashing, equality, ordering, formatting, copying, relocation, and retain/release use this traversal. Copy failure cleanup walks the completed prefix backwards with a decreasing offset. This bounds offset work per pass; nested payload work and repeated visits while queued destruction resumes remain separate costs. Strings, the most common heap field, are freed during the pass and cause no resumption.
A class with exactly one field and no __del__ is represented as that field:
same slot, same bytes, no record and no internal address. Constructing it,
reading or moving its field, copying it, and releasing it are the field’s own
operations; Wrap(file) costs nothing beyond file. Its runtime descriptor
carries the field’s layout under the class’s name, so formatting still prints
Wrap(x=1) and columnar lists still see one column. A one-field class with
__del__ needs the cleanup state word and keeps the record layout, as do
classes with two or more fields. The
flat class layout proposal records
why the record holds an address and the layout contract C interop depends on.
A field whose type is a class with a record (after looking through one-field
wrappers) is stored flat when its owner is a class record, a tuple, or an
enum variant with two or more fields: its record bytes sit at a fixed offset
in the owner’s record, with no slot word of its own. Reading, borrowing,
matching or writing owner.inner.x adds constant offsets to the owner’s record
address; moving the owner copies its bytes with no address fixes for flat
fields, so a tuple or variant of such classes moves as plain bytes. An
all-zero flat record is an empty field, so moving a field out zeroes its bytes
and releasing an empty field does nothing. The rule has these exceptions, which
keep the slot word followed by the record:
- top-level class values (locals, list elements, boxes, captures), because runtime operations and sum payloads receive one 128-bit value;
- the payload of a single-field variant, such as
Some(point)orOk(point), which is that one 128-bit value with the sum’s tags stacked in its high word: the record follows it, and moving the sum fixes only that one address, as moving a bare class value does; - field-less classes without
__del__, which have no record bytes to place.
A reference to a class value names the 16 bytes before its record whether or
not a slot word is stored there: readers use address + 16 without loading
the word, writers copy the record bytes, and writing an empty value zeroes
them. A __del__ hook receives such a reference and must not read the word.
The runtime release walk clears a finished flat record so a resumed walk skips
it.
Compiled code avoids the runtime walk when the type makes it unnecessary. A payload with no internal addresses, such as a range, a flat record, or a closure environment without inline captures, is copied as plain bytes; a sum whose payloads are all such is copied by testing its tag. An all-zero value is an empty ownership slot, such as a moved-from local, and its release makes no runtime call. These paths keep high-frequency helpers, such as error mappers called per byte, out of runtime operation calls.
Record copies and clears of up to 192 bytes are emitted inline as 16-byte
vector loads and stores rather than memmove and memset calls. A local moved
straight into a call argument, with only operand pushes between the move and
the call, is passed at its own address instead of a fresh copy: the callee
copies its arguments on entry, so the caller’s storage is not read again.
Inlined constructors, generator producers and portable SIMD operations keep
argument addresses, so moves into them still copy.
A class with a record also has a plan in its runtime descriptor: the word
offsets of the strings it owns, including those of flat class fields, whether
those strings are all it owns, and whether its bytes hold any address into
themselves. When the strings are all it owns (no __del__ at any depth, no
other managed field), the runtime releases the record by freeing those strings
directly instead of walking its fields; freeing a string runs no user code, so
the order is unobservable. A record holding no internal addresses moves without
relocation: only its owner’s value word changes. Collection rows use both, and
a list whose row class owns nothing skips its per-row release entirely.
References use two native words: an address and packed inline-sum tag-offset information. They point into typed local, frame, field, or collection storage; source loans determine when those locations remain usable. Function values are code addresses without capture environments. Returned-reference analysis caches precise field-origin summaries when available and otherwise uses conservative owner projections.
Scalar references read and write only the scalar’s declared bytes, whether they address a packed list element, a local slot, or a projected enum payload. Slot padding is not part of the scalar value; runtime equality, hashing and formatting ignore it. Projected writes leave ancestor tags unchanged.
Generator resumption reserves space for the complete inline yield layout after the runtime result word. The shared runtime-call scratch area grows for larger records and nested payloads; yielding a record does not impose a fixed-size limit.
Lists
Dense lists pack primitive numeric and boolean elements into 1-, 2-, 4- or
8-byte rows with natural alignment. Other rows contain a 16-byte value slot
and its inline payload, with no unused dictionary value slot. The runtime
keeps a 16-byte-aligned buffer, rounding allocation sizes up to whole words;
its exposed length counts initialized bytes. Borrowed scalar indexing uses
the concrete element width to calculate row counts and addresses. General
borrowed iteration obtains the runtime row stride. Native strided FFI views
use that same stride, and signed reads extend their declared bytes without a
post-call repair pass. List storage costs
describe the remaining space used by sum and record elements.
Strings
A str is one 64-bit word. Heap and literal pointers address a 32-byte prefix:
a 16-byte managed header, byte length, and scalar count, followed by UTF-8 bytes
without a terminator. Literal storage is immortal and eight-byte aligned.
A set low bit marks an inline string. Its low byte stores length << 1 | 1;
the remaining seven bytes store UTF-8. These values own no heap allocation and
retain/release ignores them. Byte equality, hashing, and printing handle both
representations identically. Source-visible text behavior belongs in
text operations.
A &substr is two
words, like a &[u8]: the address of its first UTF-8 byte and its byte count.
It has no prefix, so it is not a &str. A view of an inline string addresses
the bytes in the borrowed string’s slot, which its loan keeps in place.
Standard sums
Option and Result use a 128-bit value: a 64-bit terminal payload and a 64-bit
path of binary tags, outermost first. Nested sums add bits without boxing, bounded
by the 64-level type limit. The payload is scalar bits, an owned heap handle, or
a private address of inline data. No numeric sentinel or null pointer is reserved
as a source value. LoadError uses three terminal tag bits for seven variants.
Scalar/pointer sums use 16-byte addressable slots. Inline payload bytes follow the
value word; slots grow only when the payload requires it. Native results use
caller-owned storage, and relocation preserves sum tags. Runtime aggregate helpers
receive aligned slot pointers instead of relying on a C u128 calling convention.
A one-word operand is stored as its word and a zero word, so no two-word zero
stays live across the call (issue #757).
These layouts are private and are not the public FFI ABI.
Ranges
Ranges contain 32 bytes of bounds, step, and length. Stored ranges use 48-byte slots: ordinary value storage plus inline range data. Collection relocation and frame/result moves repair embedded addresses. Range creation and return need no separate owner allocation; see ranges.