Skip to content

Layout Reflection

Accepted

Accepted for V1 current-target .catalyst layout reflection and alignment semantics; cross-target, C ABI, packed, and other representation-mode layouts are deferred.

Source member reflection is separate from physical layout reflection:

T.layout() Type.Layout!Type.ReflectionError

T.layout() reflects Catalyst's internal .catalyst layout for the current compilation target. It is target-dependent through the active compilation target; explicit cross-target layout queries are deferred.

Because layout reflection is target-dependent, uses of T.layout() should be visible to tooling and lints, especially when public API shape, attribute metadata, or declaration guards depend on current-target layout facts.

The layout shape should include size, alignment, and physical fields:

const Layout = struct {
  size: usize
  align: usize
  fields: []const Type.LayoutField
}

const LayoutField = struct {
  name: ?[]const u8
  field_index: ?usize
  offset: usize
  size: usize
  align: usize
}

For physical fields corresponding to source-visible fields, field_index points into T.fields(). Layout-specific facts are kept in Type.LayoutField; source facts such as field type, visibility, access, defaults, and docs stay in Type.Field.

Layout metadata includes physical storage only. It includes const and mutable fields the same way because field constness affects assignment rules, not storage. It does not include type metadata such as array length.

Type.Layout.fields does not include padding entries by default. Padding can be computed from field offsets and sizes. If exact byte maps are needed for ABI or debug tooling, a future segment-oriented layout API can provide them.

V1 Representation Boundary

V1 has one accepted source representation mode:

const Repr = enum {
  catalyst,
}

Every struct type defaults to .catalyst. Explicit @repr(.catalyst) is valid but redundant.

@repr(.c) and @repr(.packed) are rejected in V1. They are not provenance-only attributes and they do not silently fall back to .catalyst. Future representation modes, C layout profiles, std.c aliases, packed layout, unaligned packed-field access rules, and compound C ABI expansion are deferred to CEP-0063: Representation Modes and C Layout.

Implementation guidance

V1 has one accepted source representation mode, but compiler implementations are strongly encouraged to route layout through an internal representation-mode abstraction. Type finalization, layout reflection, SIR, and IR should not hard-code assumptions that would make future .c or .packed support require a full rewrite.

Alignment Metadata

@align(bytes) on a struct sets an explicit minimum aggregate alignment. @align(bytes) on a field sets an explicit minimum field alignment.

Alignment values are measured in bytes. They must be nonzero powers of two and must not exceed the selected target's maximum object alignment.

V1 @align can only raise alignment:

  • an explicit field alignment lower than the field type's natural alignment is a hard error;
  • an explicit struct alignment lower than the natural aggregate alignment is a hard error;
  • lowering alignment is deferred to future representation modes such as packed layout.

Zero-sized structs and fields may be over-aligned. A zero-sized field occupies no bytes, but its offset is still rounded up to its effective field alignment and the containing aggregate alignment still considers it.

Struct Layout Algorithm

V1 .catalyst struct layout is source-order, target-dependent, and deterministic. It is not a C ABI promise and it may differ from a future @repr(.c) layout. V1 does not reorder fields for padding reduction.

For a struct with fields in source order:

  1. Start with offset = 0 and aggregate_align = 1.
  2. For each source field:
  3. compute field_size = field_type.layout().size;
  4. compute natural_align = field_type.layout().align;
  5. compute effective_align = explicit_field_align.unwrap_or(natural_align);
  6. reject if effective_align < natural_align;
  7. reject if effective_align exceeds the target maximum object alignment;
  8. round offset up to effective_align;
  9. record LayoutField { offset, size = field_size, align = effective_align, ... };
  10. advance offset += field_size;
  11. set aggregate_align = max(aggregate_align, effective_align).
  12. Apply explicit struct alignment, if present:
  13. reject if it is lower than aggregate_align;
  14. reject if it exceeds the target maximum object alignment;
  15. otherwise set aggregate_align = max(aggregate_align, explicit_struct_align).
  16. The final struct size is round_up(offset, aggregate_align).

For a struct with no fields, offset = 0, size = 0, and align = explicit_struct_align.unwrap_or(1).

Examples:

const Header = struct {
  tag: u8
  len: u32
}

On a target where u8 has size/alignment 1 and u32 has size/alignment 4, the layout is:

size = 8
align = 4
fields = [
  { name = "tag", field_index = 0, offset = 0, size = 1, align = 1 },
  { name = "len", field_index = 1, offset = 4, size = 4, align = 4 },
]

Field-level @align raises the effective alignment and may insert padding before that field:

const Header = struct {
  tag: u8

  @align(8)
  len: u32

  tail: u8
}
size = 16
align = 8
fields = [
  { name = "tag", field_index = 0, offset = 0, size = 1, align = 1 },
  { name = "len", field_index = 1, offset = 8, size = 4, align = 8 },
  { name = "tail", field_index = 2, offset = 12, size = 1, align = 1 },
]

Zero-sized fields remain visible in reflection:

const Marker = struct {}

const WithMarker = struct {
  a: u8

  @align(8)
  marker: Marker

  b: u8
}
Marker.layout().size = 0
Marker.layout().align = 1

WithMarker.layout().size = 16
WithMarker.layout().align = 8
fields = [
  { name = "a", field_index = 0, offset = 0, size = 1, align = 1 },
  { name = "marker", field_index = 1, offset = 8, size = 0, align = 8 },
  { name = "b", field_index = 2, offset = 8, size = 1, align = 1 },
]

The zero-sized marker and following b field may share the same offset because the marker occupies no bytes.

Arrays and Descriptors

Array size is element_stride * length, and array alignment is the element alignment. In V1 the stride for zero-sized elements is 0, so arrays of zero-sized elements have size 0 while preserving the element alignment:

@align(16)
const Marker = struct {}

const Markers = [10]Marker
Markers.layout().size = 0
Markers.layout().align = 16

Addresses of distinct zero-sized array elements need not be distinct.

Slices are transparent descriptors, so ([]T).layout() includes the physical ptr and len descriptor fields. Arrays do not physically store a length field; their length is type metadata.

C ABI layout is a separate future API because internal Catalyst layout and external ABI representation may differ.