For the complete Mojo documentation index, see llms.txt. Markdown versions of all pages are available by appending .md to any URL (e.g. /docs/manual/basics.md).
Register and trivial types in the lifecycle
Mojo values follow a common lifecycle, but different types participate in its stages differently. An initializer creates a value. Code may copy it, move it, or transfer it to a new owner. Finally, Mojo destroys it after its last use. Most types let you customize these stages.
Some values need almost none of that. An Int is eight bytes of integer
and nothing more: no heap memory, no open file, no reference count. Copy
its bits and you have a complete second value. Throw its bits away and
nothing is lost. For values like these, every customizable stage is a stage
that can only go wrong.
Mojo lets a type give up control of lifecycle stages it doesn't need. A register type gives up control of moving. A trivial register type gives up control of copying and destruction as well. This page follows one example of each through its lifecycle.
Values that are just their bits
A small, self-contained value can live in a register and travel between functions without a trip through memory.
Mojo calls a type register-passable when its values work this way.
Int, UInt8, Float64, Bool, SIMD vectors, and Pointer are all
register-passable. String isn't: a string's bits include the address of a
heap buffer that the string owns, so the string can't be just its bits.
Three lifecycles
This page uses two example types alongside ordinary types such as
String:
Descriptor, a register type, holds an operating-system file descriptor. Its value is one integer, but that integer names an open file that someone has to close.Sample, a trivial register type, holds one audio sample. It owns nothing beyond its bits.
Here's what each kind of type lets you control:
| Stage | Ordinary type | Register type | Trivial register type |
|---|---|---|---|
| Create | Your initializers | Your initializers | Your initializers |
| Copy | Opt in; custom copy allowed | Opt in; custom copy allowed | Implicit; always a bit copy |
| Move | Custom move allowed; can opt out | Always a bit copy | Always a bit copy |
| Transfer | Ends the original | Ends the original | Copies; the original remains |
| Destroy | Custom, generated, or explicit | Custom, generated, or explicit | Does nothing |
The rest of this page walks through those rows.
Creation
All three kinds of type create values the same way, with initializers. What differs is what the fields can hold.
Conform a struct to RegisterPassable to make it a register type:
from std.ffi import external_call
struct Descriptor(RegisterPassable):
var fd: Int32
def __init__(out self, fd: Int32):
self.fd = fd
def __deinit__(deinit self):
# The integer is small; the file it names isn't
_ = external_call["close", Int32](self.fd)
Conform a struct to TrivialRegisterPassable to make it a trivial
register type:
@fieldwise_init
struct Sample(TrivialRegisterPassable, Writable):
var channel: UInt8
var value: Float32
Every field of a register type must be register-passable, and every field
of a trivial register type must be trivial. A String field compiles in
neither. A Sample can hold other trivial values, but not a Descriptor,
because a Descriptor has cleanup to do:
@fieldwise_init
struct StereoFrame(TrivialRegisterPassable):
var left: Sample
var right: Sample
Copying
A register type copies the way an ordinary type does. It isn't copyable
until you add Copyable, and it can define its own
copy initializer when
copying means more than duplicating bits. For Descriptor, a copy would
have to open a second descriptor for the same file.
A trivial register type always copies by duplicating its bits, and it
copies implicitly. Every assignment and every argument gets its own copy,
with no copy() call. You can't write its copy initializer:
def amplify(var sample: Sample, gain: Float32) -> Sample:
sample.value *= gain # changes this function's copy only
return sample
def main():
var raw = Sample(channel=2, value=0.5)
var edited = raw # implicit copy
edited.value = 0.25 # raw keeps its own bits
var louder = amplify(raw, 2.0) # amplify() gets a copy
print(raw) # Sample(channel=2, value=0.5)
print(edited) # Sample(channel=2, value=0.25)
print(louder) # Sample(channel=2, value=1.0)
The three values never affect each other.
Moving
An ordinary type can define a custom
move initializer, or opt
out of moving with not Movable.
Register types and trivial register types give up both choices. Mojo moves
their values by copying bits, so you can't write a move initializer, and
the type can't opt out of Movable. A value with no address of its own to
protect has no reason to stay in one place.
Transfer
The transfer sigil (^) offers a value's ownership to a new owner.
For a register type, that works exactly as it does for an ordinary type.
After var keeper = log_fd^, the descriptor belongs to keeper, and
log_fd is finished. A file Descriptor owns an open file, so ownership
has real meaning and responsibilities.
A trivial value has nothing to hand over: no buffer, no file, no cleanup.
So ^ on a trivial value makes a copy, and the original stays usable:
def main():
var count = 42
var total = count^ # warning: this transfer has no effect
count += 1 # still legal; count was copied, not transferred
print(count, total) # 43 42
The compiler warns that the transfer has no effect and offers to remove the
sigil. Take the offer. A ^ tells the reader the original is finished, and
for a trivial value that isn't true. The same holds for your own trivial
types: amplify(raw^, 2.0) copies raw exactly as amplify(raw, 2.0)
does.
Save the sigil for values that own something.
Destruction
A register value dies the way any value does: at its last use, by its
deinitializer. Descriptor closes its
file there. A register type can also require
explicit destruction, so that
forgetting to release it fails to compile:
struct Lease(
RegisterPassable, not Deinitable else "call 'release()' to return the lease"
):
var id: Int32
def __init__(out self, id: Int32):
self.id = id
def release(deinit self):
# return the lease to its pool here
pass
A trivial value also dies at its last use, but nothing runs and nothing observable changes. A trivial register type can't define a deinitializer, and it can't require explicit destruction. A value with nothing to clean up has no cleanup to forget.
Trivial lifecycles without the trait
A struct doesn't have to declare TrivialRegisterPassable for its copies
and destruction to be trivial. When Mojo generates a type's copy
initializer and deinitializer, and every field is trivial, those stages are
trivial too:
@fieldwise_init
struct Pixel(Copyable):
var red: UInt8
var green: UInt8
var blue: UInt8
Pixel copies and dies as plain bits, but it isn't implicitly copyable, so
copies stay visible in code: var backup = pixel.copy().
Two things make a stage non-trivial. A field that isn't trivial, such as a
String, makes copying and destruction non-trivial. A hand-written
__deinit__() method you create does the same, making destruction
non-trivial but copying stays trivial. Only a non-trivial field, like
String, breaks both the copy and destruction stages.
Check a lifecycle in generic code
Generic code sometimes depends on a value being just its bits. Use the
comptime predicates in std.traits to ask about each lifecycle stage:
from std.traits import (
IsTriviallyCopyable,
IsTriviallyDeinitable,
IsTriviallyMovable,
)
IsTriviallyCopyable[T]is true whenTisCopyableand copying a value's bits has no side effects.IsTriviallyMovable[T]is true whenTisMovableand moving a value's bits has no side effects. This holds for most types, includingString, so copying and destruction are where types usually differ.IsTriviallyDeinitable[T]is true whenTisDeinitableand deinitialization is a no-op.
For example, this function appends a value's raw bytes to a buffer, the way a binary file writer would. Copying a value as bytes is only correct when copying it is trivial, so the function asserts it:
from std.sys import size_of
from std.traits import IsTriviallyCopyable
@fieldwise_init
struct Header(Copyable):
var magic: UInt32
var count: UInt32
def append_bytes[T: Copyable](mut buffer: List[Byte], value: T):
comptime assert IsTriviallyCopyable[T], "'T' must be trivially copyable"
var bytes = Pointer(to=value).unsafe_bitcast[Byte]()
for i in range(size_of[T]()):
buffer.append(bytes.unsafe_offset(i)[])
def main():
var buffer = List[Byte]()
append_bytes(buffer, Header(magic=0x4D4F4A4F, count=3))
print(len(buffer)) # 8
Header holds two integers, so it passes. A header with a String field
fails at compile time with the assertion's message, because its bytes
include an address that means nothing once written to a file.