Storage and Allocator#
Allocating memory does not hand you a value — it hands you an address, and the type is what says
how to read it. C++ writes that as T*; FlyDSL writes it as Storage[T], and an allocator is
anything that produces one.
allocator.allocate(T) --> Storage[T] --> .field (another Storage)
--> .peek() (a T value)
--> .poke(v) (write a T value)
Storage[T] is a universal wrapper, and it has to be. fx.Pointer cannot play this role: an MLIR
pointer’s element type must be an MLIR type, so PointerType.get(elem_ty=SomeStruct) is a
TypeError — a @fx.struct, a @fx.union, and an fx.Array are trace-time types
with no MLIR counterpart. Storage[T] therefore keeps the address in whatever pointer the allocator
produced (typically an i8 one) and carries T alongside it, in Python. Field offsets, variant
overlays, and typed loads/stores are all computed from that trace-time T, never from the MLIR
pointer type.
The layout rules a Storage navigates come from the Storable protocol. Composites acquire them by
closure over their fields — grouping is a
composite’s job, addressing is this page’s.
fx.Storage[T]: a typed address#
The correspondence with C++ is close enough to use as a lookup table:
C++ |
FlyDSL |
Note |
|---|---|---|
|
|
the address plus the type to read it as |
|
|
materialize a value |
|
|
write a value |
|
|
⇒ |
reinterpreting a union member |
|
⇒ |
Three things follow from allocate returning an address rather than a value:
the memory has no contents yet —
peek()is a load you ask for, not something allocation did for you;not every
Thas a value form — a@fx.unionnever does, so it exists only asStorage[Union]and is reached one variant at a time;a composite is not one SSA value —
Storage[T]navigates its fields by offset, which is exactly what aTvalue could not do.
peek and poke compose recursively, so a nested struct reads and writes each leaf at base + outer_offset + inner_offset.
peek and poke are real members of this class, and attribute lookup finds a member before it
reaches the type’s fields — which is why they, along with replace and any _-prefixed name, are
reserved field names.
What a Storage can point at#
T must be Storable: able to state a static size and alignment, and to be read from (and usually
written to) a traced pointer.
|
Size |
Alignment |
|---|---|---|
|
its byte width |
its byte width |
|
|
|
a composite whose non- |
see Byte layout |
see Byte layout |
Everything else is deliberately excluded, and asking for its size is a TypeError:
sub-byte numerics including fx.Boolean and fx.Int4, plus fx.Vector, fx.Pointer, and
fx.Tensor. One such field is enough to make the whole composite non-storable.
fx.Array[E, N, A]#
The fixed-size leaf: a Numeric subclass E, a positive int count N, and an optional positive
byte alignment A. Array types are cached, so the same parameters yield the same class. After
peek it behaves as a typed pointer view supporting indexing and .view(layout).
Tile = fx.Array[fx.Float32, 32, 16]
Tile.size, Tile.align # ⇒ (32, 16)
dsl_size_of(Tile), dsl_align_of(Tile) # ⇒ (128, 16)
fx.Align[T, A]#
A placement modifier, not a composite form: it delegates size and access to T and overrides only
the alignment.
Aligned = fx.Align[fx.Int32, 16]
dsl_size_of(Aligned), dsl_align_of(Aligned) # ⇒ (4, 16)
A must be a positive power of two and at least T’s natural alignment; violations are
ValueErrors, and a non-int A or a missing second parameter is a
TypeError.
Byte layout#
The offsets Storage navigates. For a product type:
start at byte offset zero;
align each field’s offset to that field’s alignment;
place the field, then continue after its size;
round the total size up to the largest field alignment.
For a union, every field is at offset zero, the size is the largest field size, the alignment is the
largest field alignment, and the size is rounded up to that alignment. Nested composites apply both
rules recursively, and Constexpr fields are skipped entirely — they have no offset.
@fx.struct
class Padded:
head: fx.Int32 # offset 0, 4 bytes
payload: fx.Align[fx.Int32, 16] # offset 16, 4 bytes, alignment 16
@fx.union
class Scratch:
fp16: fx.Array[fx.Float16, 128] # 256 bytes, align 2, offset 0
fp32: fx.Array[fx.Float32, 64] # 256 bytes, align 4, offset 0
dsl_align_of(Padded) # ⇒ 16
dsl_size_of(Padded) # ⇒ 32 — 20 bytes rounded up to the 16-byte alignment
dsl_size_of(Scratch) # ⇒ 256
Because both variants of Scratch name the same bytes, nothing validates that what one wrote is
meaningful when the other reads it — the program must establish that itself.
Allocators#
An allocator turns a Storable type into a Storage over real memory. fx.Arena is the
target-neutral bump allocator: it pads each request to the type’s alignment, hands back a
Storage[T] over base_ptr + offset, and tracks the running total in allocated_bytes. It owns no
memory of its own — base_ptr raises NotImplementedError until a subclass supplies one.
Call |
Result |
|---|---|
|
|
|
the same, with the start alignment raised to |
|
|
|
the bump cursor: everything allocated so far, including alignment padding |
Allocating a type that is not Storable is a TypeError.